Whimsical开发第二个月总结

进度

  • 新增功能
  • 产物的导出和内容的导入
  • 属性配置表单能力
  • 样式配置的模块
  • 表达式解析能力
  • 数据绑定能力及相关表达式
  • 画布自适应
  • 其他
  • 对工具包的功能进行了丰富
  • 补充部分单元测试
  • vite升级到4.0.0-beta.0版本(为了处理库打包多入口问题)
  • 引擎及组件库支持按需加载
  • 增加了性能比对工具

一些问题和思考

使用vite做支持按需加载的组件库

vite默认打包是单入口的,也就是在build.lib.entry里面只能穿一个字符串。

而支持按需加载主要是要将打包产物的内容进行分离,那这个vite也在不断做改进。

根据我现有项目的样本

  • vite2.x版本支持通过rollupOptions中的input来实现多入口分包,但是entry需要传''空字符串,来保证路径和命名符合预期。
  • vite3.2.x这个机制产生了变化,用2.x的配置进行打包会报错,因为entry不允许传''空字符串了,而在看源码的时候发现main分支已经进入到4.0.0-beta.0版本了
  • vite4.x在我使用的时候还没发布正式版,但是打包的时候build.lib.entry配置实现了rollupOptions.input相似的功能了,可以看代码段github.com/vitejs/vite…

属性配置能力选型formily

因为之前做相关建设就选用的formily,所以这次还是用了formily,但是没用全家桶,状态管理仍然使用了mobx,这里会有一些冗余,但不是当前的重点,后续再进行优化。

引擎解析性能比预计的好一些

这里测试性能的样本使用了深度为4,每个节点广度为10的全树总计10000个节点进行100次渲染的测试,下面可以看下结果。

从图中可以看出,还是多了一些解析代价的,毕竟要在运行时构建JSX接口再生成VNode,在上面的样本中有多接近10%的解析代价,在正常业务场景是可以接受的,而且这个解析思路也可以做到SSR上,那这个解析代价就会变得更小,这里还有很多细节,后面单独写文章说一下。

不过这不是最终的数据,后面随着功能的增加还会做增量的更复杂场景的对比,如:

  • 有大量表达式的时候
  • 打包文件和DSL文件因大小差异带来的优势和代价,从当前的用例来看,用DSL的包大小会比JSX打包出来的大小更大,这个跟我之前的预期是有差距的,但是这也是因为节点都是最简单的节点导致的。
  • 因状态变化带来的重新渲染的代价。

那就结而论,这个结果是比我预计要好的,在对性能要求没那么极致,但是对搭建有需求的场景,是很OK的。

12月规划

前面两个月的工作积累了一些功能,12月我会暂停新功能的开发,对项目补充单元测试和文档,争取一个月把现有的内容稳定下来。

Whimsical开发第一个月总结

进度

项目已经启动了一个月了,编辑器部分的整体进度在15%左右,进度不是很快,当前核心内容已经有了雏形,主要实现了一下功能:

  • 编辑器Layout
  • 事件调度
  • 历史快照
  • 画布分层
  • 属性配置联动
  • 设计了logo

演示

record-1m.2022-11-11 16_58_33.gif

一些技术选型

这里部分内容借鉴了formily的编辑器designable,属性配置联动部分的表单更是选用了formily进行驱动。

但是在事件调度和状态管理上方案有所不同

  • 状态管理:mobx
    • 使用mobx是为了降低项目维护的复杂度
    • mobx使用了严格模式,不允许直接通过赋值进行状态改变
  • 事件调度:rxjs
    • 当前使用rxjs是为了不去仔细考虑这里的设计,将重点放在整体框架上
    • 因为仅使用了rxjs中的subject,后续可能会对该部分进行重写
  • 拖拽系统使用了react-DND
    • 为了能更快速的开发,干脆对react-DND进行了汉化,当前是用到什么汉化什么,预计今年内会将文档全部汉化

这里没有详细写技术选型中的思考,如果有需要可以私信我,我可以多水一篇文章。

感想

项目启动至今,基本每天会抽1个小时左右写一点代码,从现在的心态上来看还OK,很充实。 中间有一段时间还有一些功利心作祟,想着无论如何要提交些什么,为了提交而提交, 现在就很释然了,尽力而为,心态放松后好像效率反而更高了。 总体来说,是个不错的尝试,当前除了时间比较少,还没有什么太大的瓶颈。

11月也会努力更新,期待感兴趣的同学和我交流。

项目地址:https://github.com/gaofeiyu/whimsical

我有一个Whimsical的项目启动了

I have a Whimsical project started

Whimsical[ˈwɪmzɪkl] 是异想天开的、古怪的、怪诞的意思,在我准备做这个项目之前并不认识这个单词,只是在想用什么作为项目名字的时候,想找个npm包中没有的名字,无意中碰到了这个词,而这个词意外地和我做这件事的目标和可执行性有些贴合,且为我要做的这件事的半途而废找到了一个很好的后路,就很完美。

我想做的东西是一个以低(零)代码编辑器为中心辐射出的一套工具包。面对这个时间点的低代码发展情况,这个赛道已经是一块让个人难以耕耘的盐碱地了,而我想做的不是一个低代码的整体平台,而是平台中每一块的零件,为低代码平台的整体架构进行解构,对页面编辑器、debugTools、设计稿自动解析、PRD自动解析、数据层等等方面输出针对的工具包和思路。

我做低代码相关的工作已经有较长一段时间了,在技术职场中,低代码是一个大多数人都用过,技术团队总想要,但又被团队十分嫌弃的一个课题。尤其是在重业务的团队,低代码相关的开发者在搞出惊天动地的结果之前,都是一直要被否定和挑战的。

不过这些困难和实践让我产生了更多的思考,于是我便启动这个项目,想将我的一些思考和实践,且跟现有工作不直接相关的内容提炼出来,期望能在更开放的平台学习和产出,解决一些低代码的通用问题。

对我的意义

这是一个从利己角度出发,产出利他结果的项目。

为什么是利己?

我是一个互联网某厂的码工,工作节奏快且充实,而长时间投入在业务和实操让我对技术最原始的驱动力变得懒惰和迟缓,简单讲就是生锈了。

我想通过这个项目来给自己一些强制的训练,复习以前的知识,并跟进新的知识,因此我在该项目中不会考虑兼容性的问题,会使用一些我感兴趣的技术栈和方案。

项目会尽可能以开源项目的思路进行一边学习一边建设,因为还有本职工作,原则上是有时间就多做点没时间就少做点,但不会不做,且有时也会为了我现有的工作当做试验场。

利他的结果是什么?

虽然这是以“我”为中心任性的项目,但是我仍然想要产出的内容有附加价值,在项目出现里程碑结果的时候,我也会进行一些运营,把我认为好的内容分享给社区,即使我的项目只是一个试错的炮灰。

比如该项目的第一个课题即是一个通用的低代码编辑器,这个通用的目标是可以融合任何技术栈的组件库的可拖拽低代码编辑器。因为个人当前的知识储备优先,因此第一阶段只面向web端,随着时间和阅历的推移,我会以整个大前端平台为目标进行学习和推进。

具体要做个什么东西

低代码是一个大课题,SaaS和aPaaS的低代码、流程图的低代码、页面搭建的低代码、低代码的上下游支撑,只从基本内容来看就能看出其建设成本之高往往让团队望而却步,这也是当前低代码相关建设被人主要诟病的原因,也是为什么很多大厂都在做低代码相关的付费服务。

如何将投入开发低代码的成本从逆差变成顺差,是每个相关开发团队需要解决的问题,这不免要进入鸡生蛋和蛋生鸡的扯皮循环。

那我想以我对低代码部分内容的理解,做出低代码整体建设中的零件,让相关团队在进行开发时少走一些弯路,或者可以直接使用我的部分成果为低代码的同僚们的KPI或OKR加把柴。

项目在当前主要涉及的内容包括:

主要内容

看到这里是不是觉得更加Whimsical了呢?

写在最后

我喜欢踢足球,但是不爱看球赛;我喜欢打dota,但是一个电竞选手都叫不出;我喜欢编程,但是已经很流行而我却不知道的技术和框架越来越多;我喜欢网上冲浪,但很少写文章,不知道看文章的你是不是像我一样,习惯把自己缩成一团。2022年里我接触了很多新东西,也尝试了不少,挫败感很多,但也总有新鲜的事物吸引我的注意力,而重要的是我舒展了自己,敢于去拥抱我自以为距离我很远的事物。

我想藉由这个项目,为我已经定型的脑袋敲出一些新花样,这篇文章可能短期不会有人能看到,但希望有那么一天会有人挖出这个文章来刺激一下未来的那个不争气的我,要么就不要异想天开,要么就坚持到底。

项目地址:https://github.com/gaofeiyu/whimsical

在qiankun微前端中做性能统计

我所在的团队中跨业务、跨平台的内容比较多,微前端越来越广泛的应用到了团队中,当前从易用性来讲还是qiankun比较适合我的团队,但是在最近看数据的时候感觉报表上的页面性能数据好的超出了我的预期,深入思考一下,猛然想到,这些该不会都只是主应用的数据,完全没统计到子应用吧。

为什么统计不到

qiankun微前端工程是主应用加载后再进行子应用文档的请求进行挂载的。

而主应用加载后子应用挂在前当前页面已经完成了LCP的指标收集,也就是说还没等主体内容被渲染,页面的性能统计行为已经结束了…

为了让数据更准确,我决定追加自定义埋点,获取更可靠的性能数据。

解决思路以及问题

在网上搜索资料的时候,竟然没有发现这方面的讨论和方案,那只能完全自己动手了。

我的目标是获取子应用完全加载后的时间是一个介于LCPTTI之间的指标。

关于页面指标的说明可以查看:https://web.dev/metrics/

想要达到这个目标,第一反应就是在页面挂载后如reactcomponentDidMountvue中的mounted进行自定义埋点,即可统计到相对准确的性能数据。

但是想要这么做需要项目使用了router统一管理路由,才能比较简单便捷的进行这个埋点,否则可能就要每个页面都做单独的埋点,或者对页面组件进行统一的封装,不是很通用。

而且还有一些更麻烦的场景,如:

  • 一些项目是使用目录方式管理路由的,没有统一的挂载入口
  • 同一个主应用不同子应用间切换,只走了挂载钩子,如何正常统计到对应的性能

下面我介绍一些我的团队实际场景中的一些解决方案。

实际方案

统一Router场景

我的团队统一router的都是react场景,其他场景请举一反三,或评论区补充。

如果你没有使用React.lazy进行页面组件的加载,那么只要在RoutercomponentDidMount时机进行自定义质量埋点即可.

那如果你使用了React.lazyrouter里面componentDidMount执行时,你的页面组件还没加载,那埋点的数据还是有误的,但是我也不想埋到每个页面上,那我还能怎么做呢?

借助Suspensefallback,示例代码如下:

import React, { Suspense, useEffect } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';

const startTime = window.performance.timing.navigationStart;

const RenderFallback = () => {
  useEffect(() => {
    return () => {
      const mountedRenderTime = +new Date() - startTime;
      // 在这里添加性能埋点代码
    };
  }, []);
  return <div>页面加载中,请稍后...</div>;
};

const Index = (): React.ReactElement => {
  return (
    <BrowserRouter>
      <Suspense fallback={<RenderFallback />}>
        <Routes>
          <Route 
            path="/test" 
            element={React.lazy(() => import('@/pages/test'))}>
          </Route>
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
};

export default Index;

什么是Suspense

主要是用来配合React.lazy进行使用的,在lazy的组件未加载好之前,通过fallback中的内容进行临时的填充如loading,详细内容参考:https://zh-hans.reactjs.org/docs/react-api.html#reactsuspense

通过目录自动产生路由的场景

这里以vuenuxt的场景举例,其他场景请举一反三或大家评论补充。

nuxt这个场景前面有提到,因为没有显式的路由组件进行声明,通过上面的思维就无法解决这个问题了,因此探索了一下其他的方案,我最终选择了为nuxt增加plugin的方式进行处理。

大概思路是为全局注入一个mixin,如果当前是page组件就在mounted的时候进行埋点操作。

实现步骤

  1. 我在nuxt.config.js文件中,增加了一个performance.ts的配置,并且关闭ssr

plugin文档请参考:https://nuxtjs.org/docs/configuration-glossary/configuration-plugins#the-plugins-property

plugins: [
    {
      src: '@/plugins/performance-tracker.ts',
      ssr: false
    }
]
  1. 编写performance.ts
import Vue from 'vue'

declare global {
  interface Window {
    __POWERED_BY_QIANKUN__: any
  }
}

const startTime = window.performance.timing.navigationStart
const startFromMainApp = !!window.__POWERED_BY_QIANKUN__

let SUBMIX_PERFORMANCE_TRACKER_DONE = false

Vue.mixin({
  mounted() {
    if (!(this as any).$parent && !SUBMIX_PERFORMANCE_TRACKER_DONE) {
      SUBMIX_PERFORMANCE_TRACKER_DONE = true
      const mountedRenderTime = +new Date() - startTime
      // 在这里添加性能埋点代码
    }
  }
})

在代码中有个我通过判断某个组件的this.$parent为空,来确定这是个是当前页面的page组件,而在埋点后,还要记录一下埋点状态,避免场景考虑的不周到带来的误埋点。

单页面应用性能统计

子应用间切换跟单页面跳转和上述情况还不太一样,因为会重新请求资源,子应用也要重新渲染,不关注性能也会有很大的隐患,但是乾坤本身给到的挂载钩子并不能满足统计的需要,那我如何统计呢?

上面两个方案解决了第一次进入应用时的解决方案,但是单页面应用和子应用间切换时又统计不到了。

可以在页面卸载的时候,在子应用全局注入一个新的页面起始时间,以此来进行子应用切换场景的性能埋点。

我还是以nuxtplugin代码为例。

import Vue from 'vue'

declare global {
  interface Window {
    __POWERED_BY_QIANKUN__: any
  }
}

let startTime = window.performance.timing.navigationStart
let startFromMainApp = !!window.__POWERED_BY_QIANKUN__

let SUBMIX_PERFORMANCE_TRACKER_PATH = window.location.pathname
let SUBMIX_PERFORMANCE_TRACKER_DONE = false

const startRenderTime = +new Date() - startTime

Vue.mixin({
  mounted() {
    if (!(this as any).$parent && !SUBMIX_PERFORMANCE_TRACKER_DONE) {
      SUBMIX_PERFORMANCE_TRACKER_DONE = true
      const mountedRenderTime = +new Date() - startTime
      
    }
  },
  destroyed() {
  	// 在当前页面组件销毁时进行性能埋点的前置准备
    if (SUBMIX_PERFORMANCE_TRACKER_PATH !== window.location.pathname) {
      SUBMIX_PERFORMANCE_TRACKER_PATH = window.location.pathname
      SUBMIX_PERFORMANCE_TRACKER_DONE = false
      // 这里可以在全局缓存一个状态表示下一个挂载的子应用是被切换过去的,这个也可以通过主应用标记
      startFromMainApp = false
      // 这里可以将startTime注入到全局给其他子应用进行获取,进行子应用切换的性能统计
      startTime = +new Date()
    }
  }
})

单页面应用因为往往做了应用级别的预加载,后续页面的加载性能不是一个很复杂的课题,课题往往在页面中的交互,所以性能这部分的性能根据场景也可以选择性忽略。


单页面应用其实和微前端关系不大,只是给子应用切换做一个铺垫,下面我们来看下子应用切换的性能统计。

子应用切换的性能统计

子应用切换的成本比单页面切换的成本高不少,因为新挂载的子应用的行为,会重新请求资源,且重新渲染,不关注性能也会有很大的隐患,但是乾坤本身给到的挂载钩子并不能满足统计的需要,那我如何统计呢?

那经过前面单页面应用性能统计的启发,统计是单页面应用统计的一个升级版,差异在于需要在主应用上更新启动时间。

这个时候如果你的项目有类似的需要就封装一个更新时间的API通过给到子应用,或者干脆将主应用的window传递给子应用(不推荐)。

window.updateStartTime = function(newTime) {
  window.SUBAPP_START_TIME = newTime;
};

// 给子应用配置要传递的方法
{
  props: {
    updateStartTime: window.updateStartTime,
    // otherProps
  }
}

结语

至此,虽然我列举的场景可能不全,但是通过上面的方案对有需要的同学应该能提供一些灵感,如果你有更好的方案可以在评论区给出你的想法。

自从上线了这套自定义性能统计的埋点,才发现之前的性能数据真的都是虚假繁荣,而没有很快发现,也是因为qiankun主应用的性能本身也是个问题,先说到这里,我要和同学们琢磨优化的事情去了。

项目代码规范

接手的业务和团队走过了一段适应期,想要对团队的代码风格开始进行规范,在渐进落地后整理出一套比较通用的方案。

这里主要描述结论,选型和团队中方案PK的流程暂且先跳过,后面单独起一个专题讨论。

我的团队技术栈主要是React+TS,比较有针对性,因此请选择性食用。

下面的规范主要分美化和lint两个部分,且只讨论自动的部分,不讨论风格指南相关,那我们直接开始。

Lint

什么是Lint?

什么是Lint?

In computer programming, lint is a Unix utility that flags some suspicious and non-portable constructs (likely to be bugs) in C language source code; generically, lint or a linter is any tool that flags suspicious usage in software written in any computer language. The term lint-like behavior is sometimes applied to the process of flagging suspicious language usage. Lint-like tools generally perform static analysis of source code.

ESLint

lint的核心毫无疑问是eslint,但是eslint中用哪些plugin就比较有讲究了。

必须:

可选:

StyleLint

css出现bug的几率比较小,不太容易引起人们的重视,但其实css亦是组件质量的重要部分,在很多场景仍然要消耗大量的开发成本,因此有个好的写css习惯是非常重要的。

我们的项目中并没有严格采用BEM(窟窿比较多,这个优先级很低),因此为了让有好习惯的同学看其他人的代码不会太痛苦,就采用了StyleLint,StyleLint也是前端项目必备的Lint。

这里不需要太多的配置,在我们的项目中仅使用了stylelint-config-idiomatic-order插件来控制大家代码的css样式顺序。

美化

Prettier

团队中对使不使用Prettier有一些争议,不建议使用的主要观点是认为eslint可以cover这一块了,那针对这个疑问和一些其他疑问下面给了一些思考和解答:

module.exports = {
  // 行宽,即一行内容超过配置的数字会触发换行行为
  printWidth: 80,
  // 缩进数
  tabWidth: 2,
  // 缩进是否用tab,如果不是则用空格
  useTabs: false,
  // 行尾是否有分号
  semi: true,
  // 是否要使用单引号
  singleQuote: false,
  /**
   * 对象中的Key展示引号的规则
   * as-needed: 仅在必要时展示
   * consistent: 如果对象中有属性需要引号,那么就都要使用
   * preserve: 使用你本来的输入
   */
  quoteProps: 'as-needed',
  // jsx 是否使用单引号
  jsxSingleQuote: false,
  /**
   * 末尾逗号
   * es5: 在es5中的数组和对象增加尾随逗号,TS中的类型参数不会追加
   * none: 没有尾随逗号
   * all: 尽可能使用尾随逗号
   */
  trailingComma: 'es5',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // 标签的尖括号是否要跟随到最后一行的末尾,而不是单独一行(不包括自闭合)
  bracketSameLine: false,
  /**
   * 箭头函数参数周围的括号
   * always: 始终需要括号
   * avoid: 尽可能省略括号
   */
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 是否需要在文件开头写 @prettier
  requirePragma: false,
  // 是否在文件开头插入 @prettier
  insertPragma: false,
  /**
   * 折行方式
   * always: 超过print width则换行
   * never: 每个文本块为一行
   * preserve: 保持输入的原样
   */
  proseWrap: 'preserve',
  /**
   * HTML的空白换行敏感度
   * css: 根据css的标准进行换行
   * strict: 所有标签周围的空格都会造成换行
   * ignore: 所有标签周围的空格都忽略
   */
  htmlWhitespaceSensitivity: 'css',
  /**
   * 换行符
   * lf: \n mac or linux or git repos
   * crlf: \r\n windows
   * cr: \r 不太常用
   * auto: 保持现有的换行
   */
  endOfLine: 'lf',
  // 是否强制每行单个属性
  singleAttributePerLine: false
};
  • 和其他lint配合使用,值得一提的是,配合使用的方式就是将prettier的配置整合到对应lint中,也就是说不使用prettier也将是一种可能。

工作流相关工具

除了增加lint和美化的配置来让辅助让代码符合规范,为了让代码更确切的能达到规范的目的,我们还希望可以在提交代码的时候进行一次自动验证并进行fix,因此还需要一些其他的工具。

Lint-staged

在代码提交前进行lint,以此来强制仓库的代码格式,这个lint是对当前提交增量的文件进行lint的,官网:https://github.com/okonet/lint-staged

// package.json 
{
  // ... other config
  "lint-staged": {
    "**/*": "prettier --write --ignore-unknown", //格式化
    "src/**.{js,jsx,ts,tsx}": "eslint --ext .js,.jsx,.ts,.tsx", //对js文件检测
    "**/*.{less,css}": "stylelint --fix" //对css文件进行检测
  }
}

Husky

在git执行的生命周期钩子中追加能力,官网:https://typicode.github.io/husky/#/

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

echo $PATH
npm run test
  • 命令输出后 会展示当前的环境变量,如果命令找不到说明命令不在环境变量中

CommitLint

对提交的commit的message进行lint,官网:https://commitlint.js.org/#/

如果自己不会写,或者想增加更规范化的提交消息,可以再增加个Commitizen

commitlint相关包的依赖关系

其他工具

koroFileHeader

https://github.com/OBKoro1/koro1FileHeader.git

这是一个中国开发者制作的VSCode的插件,用来自动往文件头部写文件被操作的信息的,非常实用。

支持很多自定义配置,我现在使用这个工具主要记录文件创建者+创建时间+文件最后修改者+修改时间信息。

页面FPS分析

chrome现在有一个高大上的fps观测工具,你可以不用再去分析performance面板的跑分了。

开启FPS工具

方法一

打开devTools,如果你是mac请按组合键 commend + shift + p (win系统自行翻译),然后再搜索框中输入fps,你会看到如下的状态。

选择第一个结果回车即可

方法二

在更多工具的Rendering面板下面,选中“Frame Rendering Stats”即可

这个工具有个缺点是位置不能移动。

根据工具分析FPS

看下面这张图就够了

总结来说就是:

  • 左上角的百分比越高越好
  • 红色块越少越好
  • 红色块越均匀越好

参考资料:

chrome工具之performance面板

前言

你可以通过performance标签下面的learn more,

或者直接跳转

来学习官方的教程。

这里对重点部分做一个摘要来方便你的快速使用。

生成性能数据

1、在DevTools中,单击记录 。当页面运行时,DevTools捕获性能指标。

2、等待一段时间,期间可以执行一些自己想要检测的操作

3、点击红点或者点击stop停止录制查看结果

简单分析数据

官方已经写得很通俗易懂了不需要再概括了,请直接看官方文档

https://developer.chrome.com/docs/devtools/evaluate-performance/reference/

文中也给了一些优化建议,有兴趣也可以看看。

注:教程中fps分析部分已经过时这里可以查看页面FPS分析

chrome工具之memory面板

原始版本的工具叫profiles,chrome87+以后一部分改为了memory和performance。

开发过程中难免会遇到内存问题,本次笔记主要是记录一下项目运行过程中内存情况的解析工具。

查看页面在浏览器中使用的内存

如果只想查看当前浏览器的各个 tab 正在使用的内存量,则在设置 - 更多工具 - 任务管理器即可。效果如下图:

Memory 面板介绍

上面有三个按钮:

  • Heap snapshot – 用以打印堆快照,堆快照文件显示页面的 javascript 对象和相关 DOM 节点之间的内存分配
  • Allocation instrumentation on timeline – 在时间轴上记录内存信息,随着时间变化记录内存信息。
  • Allocation sampling – 内存信息采样,使用采样的方法记录内存分配。此配置文件类型具有最小的性能开销,可用于长时间运行的操作。它提供了由 javascript 执行堆栈细分的良好近似值分配。

下面详细介绍一下三个功能(时间关系这里直接粘贴了原文内容)

Heap snapshot

给个 html,里面只有一句 js 代码 var _____testArray_____ = [ {value: 'hello'} ];,打个堆栈看看:

右上那块区域,从左到右有三个操作:查看方式、对象归类的筛选、对象选择。

左边有 Summary 字样的那个,可以选择查看内存快照的方式,可选方式如下:

  • Summary – 可以显示按构造函数名称分组的对象。使用此视图可以根据按构造函数名称分组的类型深入了解对象(及其内存使用),适用于跟踪 DOM 泄漏。
  • Comparison – 可以显示两个快照之间的不同。使用此视图可以比较两个(或多个)内存快照在某个操作前后的差异。检查已释放内存的变化和参考计数,可以确认是否存在内存泄漏及其原因。
  • Containment – 此视图提供了一种对象结构视图来分析内存使用,由顶级对象作为入口。
  • Statistic – 内存使用饼状的统计图。

附上 Comparison 效果,大致如下:

代码:

var _____testArray_____ = [{ value: 'hello' }]

function someTodo() {
  _____testArray_____.push({
    value: ':::::::::'
  })
}

document.querySelector('#btn').addEventListener('click', someTodo, false)

点击按钮后,数组中 push 了新的一项对象

图(array 那块列表展开就看不到下面列表了,就没展开):

附上 Containment 视图,它的排列稍微有些不同,大致如下:

入口有:

  • DOMWindow – 是被视为 JavaScript 代码 “全局” 对象的对象。
  • GC – VM 的垃圾使用的实际 GC 根。GC 根可以由内置对象映射、符号表、VM 线程堆栈、编译缓存、句柄作用域和全局句柄组成。
  • 原生对象 – 是 “推送” 至 JavaScript 虚拟机内以允许自动化的浏览器对象,例如 DOM 节点和 CSS 规则。

中间的 Class filter 只能够按照列出来的 Constructor 值进行筛选。

右边的 All objects 能够选择查看哪些阶段的对象、如”Objects allocated before Snapshot1″、”Objects allocated between Snapshot1 and Snapshot2″

右中那块区域显示的内存快照信息,可以在各个数据上右键选择一些操作( Reveal in Summary view ),各个字段代表信息如下:

  • Contructor – 表示使用此构造函数创建的所有对象
  • Distance – 显示使用节点最短简单路径时距根节点的距离
  • Shallow Size – 显示通过特定构造函数创建的所有对象浅层大小的总和。浅层大小是指对象自身占用的内存大小(一般来说,数组和字符串的浅层大小比较大)
  • Retained Size – 显示同一组对象中最大的保留大小。某个对象删除后(其依赖项不再可到达)可以释放的内存大小称为保留大小。
  • New – Comparison 特有 – 新增项
  • Deleted – Comparison 特有 – 删除项
  • Delta – Comparison 特有 – 增量
  • Alloc. Size – Comparison 特有 – 内存分配大小
  • Freed Size – Comparison 特有 – 释放大小
  • Size Delta – Comparison 特有 – 内存增量

右下那块区域显示的是被选中对象的详细信息,如上面图片的内容一样一样的…可以在各个数据上右键选择一些操作( Reveal in Summary view )。

注意:图中最最下面那块最有用,就是搜索,ctrl/command + f 唤出

最后,根据上面的图来分析一下上面代码产生的效果,根据 js 的类型和引用的关系来分析,变量_____testArray_____ 在列表中的情况是:

  • 基础类型 string 值为 hello ,内存标记是 string@353953,这个 string 值存在于 Object @362113 对象上的 value 属性上;
  • Object @362113 在 Object 列表里,在 Array @356493 的索引 0 位置存在该对象的引用;
  • Array @356493 在 Window / @353829 对象上存在引用,属性名为”___testArray___”;
  • Window / @353829 是个 Windows 对象,在 Windows 列表里。
"hello" -> 在(string)列表里 -> string@353953 -> value in Object @362113

    Object  -> 在 Object 列表里 -> [0] in Array @356493

        Array -> 在(array)列表里 -> _____testArray_____ in Window / @353829

                Windows ->  在 Windows 列表里 -> Window / @353829

Allocation instrumentation on timeline

看完静态的快照,再来看看动态的。

代码如下:

var _____testArray_____ = [{ value: 'hello' }]
var count = 1

function someTodo() {
  // 每次点击 字符串长度都以上一次为基础增加到5倍,拉大差异突出效果,并且之后在字符串头部加上count值做区分
  count *= 5
  var str = new Array(count * 10).join(':')
  _____testArray_____.push({
    value: count + str
  })
}

document.querySelector('#btn').addEventListener('click', someTodo, false)

选择 Allocation instrumentation on timeline 点击开始记录的按钮,然后得到如图所示:

每条线的高度与最近分配的对象大小对应,竖线的颜色表示这些对象是否仍然显示在最终的堆快照中。蓝色竖线表示在时间线最后对象仍然显示,灰色竖线表示对象已在时间线期间分配,但曾对其进行过垃圾回收。(这图中不是很明显,放大 devtool 面板后,图中的蓝色线顶部是有部分是灰色的…)

可以选择时间范围,查看该时间范围内的内存变化情况,如上图 5 次变化的情况分别是:

# 前面的数字代表本次记录索引,点击了5次

# 0  Shallow Size : 112
Constructor         Distance   Shallow Size    Retained Size
- - - - - - - - - - - - - - - - - - - - - - - - -  - - - - - - - - - - -
(array)×9                3      5008  0%         5008  0%
(system)×60              3      2416  0%         2640  0%
(closure)×1              3      4768  0%         2928  0%
Object×3                 3      144   0%         768   0%
MouseEvent×3             4      112   0%         7200  0%
(string)×2               5      96    0%         96    0%
(concatenated string)×2  4      64    0%         160   0%
Event                    5      56    0%         2040  0%
UIEvent                  5      32    0%         648   0%

# 1
(string)×2               5      296   0%         296   0%
(concatenated string)×2  4      64    0%         360   0%
Object                   3      32    0%         392   0%

# 2
(string)×2               5      1296  0%         1296  0%
(concatenated string)×2  4      64    0%         1360  0%
Object                   3      32    0%         1392  0%

# 3
(string)×2               5      6296  0%         6296  0%
(concatenated string)×2  4      64    0%         6360  0%
Object                   3      32    0%         6392  0%

# 4
(string)×2               5      31296 0%         31296 0%
(array)                  4      80    0%         80    0%
(concatenated string)×2  4      64    0%         31360 0%
(system)                 4      32    0%         32    0%
Object                   3      32    0%         31392 0%

当勾选 Record allocation stacks 框后,还可以在 Allocation stack面板里打印出调用堆栈。

如上面代码的效果:

Allocation sampling

这个功能仅根据名称和说明的话,不是很能看得懂是什么…

但是,还是通过一些案例给出了效果图,可以看出这块的功能应该是:哪些函数影响了内存的分配,并且该函数所耗内存在内存分配中占比多少。效果图如下:

图中函数可以直接点击跳转到函数定义的文件和位置。

参考资料

前端主要性能的相关指标

这里主要以谷歌的技术文章为依据,此处主要是对参考文章的总结和摘要。

https://web.dev/metrics/ 中已经有更细节的指标描述,而且做了中文版本的翻译,方便大家更好的学习和参考。

定义指标

为了确保用户体验,我们通常围绕以下问题来进行性能的相关优化:

Is it happening?发生了么Did the navigation start successfully? Has the server responded?导航是否成功启动?服务器响应了吗?
Is it useful?是有用的么Has enough content rendered that users can engage with it?是否渲染了足够的内容给用户?
Is it usable?可以使用了么Can users interact with the page, or is it busy?用户可以与页面进行交互,还是页面仍在忙碌中?
Is it delightful?流畅么Are the interactions smooth and natural, free of lag and jank?交互是否流畅,没有延迟和卡顿?

如何度量

  • 通过实验环境进行测试
  • 通过实际生产环境进行测试

指标类型

  • 感知的加载速度:页面加载并将其所有可视元素呈现到屏幕的速度。
  • 加载响应度:页面加载和执行任何必需的JavaScript代码以使组件快速响应用户交互的速度
  • 运行时响应性:页面加载后,页面对用户交互的响应速度有多快。
  • 视觉稳定性:页面上的元素是否会造成用户不期望的方式移动,并可能干扰他们的交互?
  • 平滑度:过渡和动画是否以一致的帧速率渲染并从一种状态流畅地流动到另一种状态?

重要的衡量指标

  • First contentful paint (FCP): measures the time from when the page starts loading to when any part of the page’s content is rendered on the screen. (labfield)
    • 第一个内容的绘制:测量从页面开始加载到屏幕上呈现页面内容的任意部分的时间。
  • Largest contentful paint (LCP): measures the time from when the page starts loading to when the largest text block or image element is rendered on the screen. (labfield)
    • 最大内容的绘制:测量从页面开始加载到屏幕上最大的文本块或图像元素被渲染的时间。
  • First input delay (FID): measures the time from when a user first interacts with your site (i.e. when they click a link, tap a button, or use a custom, JavaScript-powered control) to the time when the browser is actually able to respond to that interaction. (field)
    • 首次输入的延迟:测量从用户第一次与您的站点交互(即,当他们单击链接、点击按钮或使用自定义的、JavaScript驱动的控件)到浏览器实际能够响应该交互的时间。
  • Time to Interactive (TTI): measures the time from when the page starts loading to when it’s visually rendered, its initial scripts (if any) have loaded, and it’s capable of reliably responding to user input quickly. (lab)
    • 互动时间:衡量从页面开始加载到可视化呈现之间,它的初始脚本(如果有)已加载以及可以可靠快速地响应用户输入的时间。
  • Total blocking time (TBT): measures the total amount of time between FCP and TTI where the main thread was blocked for long enough to prevent input responsiveness. (lab)
    • 总阻塞时间:测量FCP和TTI之间的总时间。如果主线程被阻塞的时间足够长,会阻止输入响应。
  • Cumulative layout shift (CLS): measures the cumulative score of all unexpected layout shifts that occur between when the page starts loading and when its lifecycle state changes to hidden. (labfield)
    • 累积布局偏移:衡量在页面开始加载到页面生命周期状态 变为隐藏之间发生的所有意外布局转移的累积分数。
    • 这个性能分数和其他指标不太一样,该指标依赖视觉及交互相关的设计
    • 计算公式:layout shift score = impact fraction * distance fraction
  • Frames Per Second(FPS): 是指画面每秒传输帧数
该处的内容会有一个专题来描述每个指标具体怎么看。

指标的参考值

注意参考值中的“参考”,表示以下标准适用于大多数场景,但具体情况仍需要具体分析,不可一概而论。

  • FCP:1秒内
    • To provide a good user experience, sites should strive to have First Contentful Paint occur within 1 second of the page starting to load. To ensure you’re hitting this target for most of your users, a good threshold to measure is the 75th percentile of page loads, segmented across mobile and desktop devices.
  • LCP:2.5秒内
    • To provide a good user experience, sites should strive to have Largest Contentful Paint occur within the first 2.5 seconds of the page starting to load. To ensure you’re hitting this target for most of your users, a good threshold to measure is the 75th percentile of page loads, segmented across mobile and desktop devices.
  • FID:100毫秒以内
    • To provide a good user experience, sites should strive to have a First Input Delay of less than 100 milliseconds. To ensure you’re hitting this target for most of your users, a good threshold to measure is the 75th percentile of page loads, segmented across mobile and desktop devices.
  • TTI:5秒内
    • To provide a good user experience, sites should strive to have a Time to Interactive of less than 5 seconds when tested on average mobile hardware.
  • TBT:300毫秒以内
    • To provide a good user experience, sites should strive to have a Total Blocking Time of less than 300 milliseconds when tested on average mobile hardware.
  • CLS:分数小于0.1
    • To provide a good user experience, sites should strive to have a CLS score of less than 0.1. To ensure you’re hitting this target for most of your users, a good threshold to measure is the 75th percentile of page loads, segmented across mobile and desktop devices.
  • FPS: (注:该数据没有找到十分权威的来源,如果你有更权威的来源可以@feiyugao进行补充)
    • 在网页中,帧率能够达到50~60fps的动画将会相当流畅,让人倍感舒适。
    • 帧率在30~50fps之间的动画,因各人敏感程度不同,舒适度因人而异。
    • 帧率在30fps以下的动画,让人感觉到明显的卡顿和不适感。
    • 帧率波动很大的动画,亦会使人感觉到卡顿。

参考资料