前端如何做性能建设?组件库『筛查、归因、修复、监测』全流程实践

代强张玉梅2024/04/07

背景

Semi Design 从 2019 年开始,作为抖音 BU 中后台类业务底层设施的一部分,为各个业务的快速迭代提供了稳定的支持,经过数年的持续优化,Semi 在功能性、稳定性和健壮性上已经相对成熟,同时我们也借助单元测试、E2E测试,视觉对比测试 三类主流的工具建立了一套稳定的质量保障机制。 但在一些业务方的重度使用场景里,我们也注意到了一些不足和待优化的部分:Semi 在性能建设方面仍缺乏系统性的保障机制,只做了一些单点的优化,较为初阶。因此,从 2023年 Q4 开始,我们针对组件的性能改进及如何建设完善的防劣化机制开始做了专项系统投入

在团队 UI 组件库的性能建设中,业界有不少经验沉淀和共识,例如

  1. 减少组件的更新:在组件状态发生变化时,减少不必要的组件更新。
    • 减少渲染的节点,只更新必要的部分。
    • 降低组件渲染的复杂度,避免不必要的计算和操作。
    • 使用缓存来避免重新渲染,使用React的 PureComponent、React.memo、useCallback和useMemo等方法,缓存节点数据,减少计算与渲染次数。
  2. 优化逻辑,减少不必要的计算和操作,并优化 CSS 样式,减少布局和绘制的成本。
  3. 代码分割和按需加载:将整个UI组件库进行代码分割,只加载使用到的组件,而不是一次性加载全部组件。减少初始加载的资源量,提高应用的加载速度。
  4. 使用性能分析工具对组件库进行性能检测:使用一些常用的性能分析工具包括 Chrome Dev Tools、Lighthouse、Webpack Bundle Analyzer等,检测代码的性能瓶颈,并进行优化。

这些方法在实践中起到了很大程度的作用,保证了 UI 组件库的性能基线。但对于很多场景,受限于需求的复杂性、工期和代码实现成本,仍然存在一些性能上的妥协。在业务长期实践中,也暴露出了一些不足与待优化的点,主要存在以下问题

  • 在大数据场景下的部分组件的计算耗时较长
  • 一些组件频繁的 UI 变化在页面大量 dom 的场景下会导致页面进行不必要的回流与重绘
  • 动画实现逻辑中大量依赖 js 引擎计算关键帧位置,在页面有复杂 js 任务下,动画线程被占用导致掉帧,且无法使用 GPU 进行并行加速
  • 事件的监听和移除依赖于手工代码,可能出现遗漏,难以发现并导致 Memory Leak

针对于上述问题,我们采取了一系列措施,确保代码质量和性能

问题筛查

通过对代码进行筛查,结合历史用户反馈和研发开发经验,发现组件中存在的性能问题并着手分析和归纳。

用户历史反馈

Semi 从字节跳动各业务线的复杂场景提炼而来,服务于公司互娱、产研等多个部门,在使用过程中,不断收到各类用户的积极反馈,我们认为从用户视角发现产品的不足与缺陷是相对高效且贴近实际生产的方式。

回流重绘

监听器漏移除

动画性能

多余计算

其他

...

排查方向

通过对现有代码进行筛查,并结合经验,列举了组件库代码中的常见错误,供参考。

基础性能指标

内存

  • 减少内存申请
    • 是否有不必要的临时变量的创建
    • 是否不必要地对用户入参 Props 进行 clone
    • 是否有连续 map 或 连续 filter 等数组方法的不必要链式调用
    • 是否存在可以使用事件委托但没有的代码
  • 及时释放
    • 是否有忘记清理的回调导致闭包变量未被释放
    • 是否有使用 promise 在组件卸载后仍然没有 fulfilled或者 rejected
    • 是否有申请全局变量或向全局变量对象上挂载属性和方法但组件卸载时并未清理
    • 是否有使用 setTimeout 或 setInterval 等回调函数但未清理

Rerender 次数

  • 减少状态更新次数
    • 组件中是否存在通过一个刚刚被改变的 state 计算出另一个 state 再 setState 的代码
    • 组件中是否存在可以使用 shouldComponentUpdate 来屏蔽父级更新导致更新的优化点
    • componentDidUpdate 中是否存在根据 props 更新 state 的逻辑,是否可以提取到 getDerivedStateFromProps
  • 优化状态更新时计算逻辑
    • 有没有使用缓存的可能
    • 初始状态的计算是否可以提前
    • 是否可以 delay 计算到 requestIdleCallback 中

帧数

  • 是否一定有必要使用 JS 计算动画帧?使用 CSS 可以获得 GPU 加速且不占用 JS 引擎
  • 如果必须使用 JS 计算动画,是否放入了 requestAnimationFrame
  • 这里的动画是必要的吗?耗性能的动画是否提供了开关供用户关闭

敏感组件特征

性能问题组件或有性能问题的场景大多具有相同的特征,通过特征也可以判断出哪些组件可能会有性能问题,可用于指导未来新增组件的性能跟踪工作

  • 场景涵盖大数据,用户输入数据较大
  • API 繁多,组件内部计算复杂,判断较多
  • 组件交互涉及大量 DOM 更改
  • 场景涉及频繁的样式变化,不断触发自身重绘和相同渲染层元素回流

针对上述问题和特征,我们对组件做了全面的分析与检查,并针对发现的问题做了专项修复与优化,总结了相关经验和结论,记录在下方。

优化实操

动效

Semi 在旧版本所有的动画采用 js 实现,通过 js 计算关键帧位置后通过渐变函数计算每帧具体样式,导致在页面较复杂时,计算速度无法保证匹配显示器刷新率,会掉帧。

场景

在动画计算过程中,如果涉及的 dom 较多,样式计算较复杂时,页面动画会掉帧,体感卡顿,影响用户交互体验。

原因

js 动画即使采用了 requestAnimationFrame 保证动画计算在每帧都触发,但 js 引擎计算速度无法保证在帧间时间能够计算完毕,导致整体网页观感刷新率降低,产生掉帧感。

解决方案

通过实现方式的改变,Semi 将原先绝大部分的 js 动画都替换为了 CSS 动画实现,将动画帧的样式计算交由浏览器渲染引擎,不仅保证了在 js 进程被阻塞时,动画仍然能流畅执行,也保证了显卡资源能够被充分利用,计算获得了 GPU 加速。

效果

以 Modal 为例,在 Modal 内放置 form,并在 form didmount 时进行 validate 模拟大量计算

  • 复现前提: 注意,浏览器引擎会将频繁执行的 js 逻辑 标记为常用,并预编译为字节码,供下次调用提高性能。这种行为会影响测试对比。务必在 modal 多次打开关闭后,再执行性能录制。
  • 数据来源
    • css 动画帧数来源: 开发者工具
    • js 动画帧数来源: 在 modal 动画代码中,变更dom 样式时,log 出时间戳,则 FPS = 输出次数/(结束时间-开始时间)
Form field count / FPSJSCSS
1004~960
502960
306560
显示第 1 条-第 3 条,共 3 条

结论

CSS 动画在相同场景,性能上明显优于 JS 动画。

其中,虽然 js 动画在简单场景下帧数超过了 css 动画 (65 > 60),但无意义,因为用户设备屏幕刷新率是用户感知上限(非游戏特化 PC一般为 60hz,移动端普遍 120 hz ~ 165hz 以上),虽然 js 动画帧数为 65hz,但用户视觉观感与 60 hz 一致,CSS 动画的 60hz 反而更加节省设备电量。

Cascader

场景

当 Cascader 开启 leafOnly,可搜索,受控 value 情况下,在几千级别的数据被同时选中,或者进行搜索时候,会出现页面卡顿问题

原因

使用场景中存在多处的多余计算:

  1. 在选中时,需要通过回调函数对外透出被选中的 value,由于选中状态通过唯一标识 key 进行保存,因此需要借助内部状态变量 keyEntities 完成从 key 到 value 的映射,为避免映射映射过程中的 TypeError,使用了 lodash 的 isEmpty 判断值是否为空, N 个节点被选中,会存在 2N 次 isEmpty 判断(一次判断 keyEntities,一次判断从 keyEntities 中 key 对应的值)。一次 lodash 的 isEmpty 调用包含多次判断(是否为 null、Array、map、set、原型、对象等),大数据量情况下耗费时间较多
  2. 搜索场景下使用 TagInput 作为触发器,在处理选项在 TagInput 显示和删除逻辑时存在从 key 找 value,value 找 key 的过程,N个节点选中,则存在 N 次遍历 keyEntites 的操作
  3. 受控逻辑下,需要通过 value 计算内部选中状态对应的 key 值, N 个选中项, 存在 N 次遍历 keyEntites 的操作

解决方案

  1. 移除多余的 isEmpty 判断。 keyEntities 在代码初始化以及各类计算中兜底值为空对象,移出对 keyEntities 的 isEmpty 判断,对于从 keyEntities 中取出来的值是否存在,简化为一次是否为 undefined 判断
  2. TagInput 中,传入 key 数组代表已选项,通过 TagInput 的自定义渲染选项 API 处理节点渲染。去除 N 次遍历 keyEntities 的操作
  3. 改造 Cascader 的 key 生成规则,从按照选项在各层级中的顺序生成 key 修改为通过级联 value 生成 key,将从级联 value 找 key 的时间复杂度从 O(n) 缩减为 O(1)
场景测试项目改造前改造后
leafOnly1k 节点选中事件处理时长400ms24ms
leafOnly + 可搜索在leafOnly 场景改造基础上,1k 节点选中事件处理时长980ms26ms
leafOnly + 可搜索+受控 value在leafOnly + 可搜索场景改造基础上,1k 节点选中事件处理时长923ms46.38ms
显示第 1 条-第 3 条,共 3 条

Tree

问题场景

大量节点渲染;大数据场景中的展开、多选操作;大数据量 treeData 且数据存在 value 项的受控使用。

原因

  1. 对于大量节点渲染场景,由于渲染结构采用树形嵌套结构,Tree 数据所有节点每次都会被渲染为TreeNode,导致大量节点渲染时占用时间长
  2. 无虚拟化,需要同时渲染的内容多
  3. 对于展开,多选场景,Tree 的状态数据都存储在拍平的 map 中,每次计算都要通过递归重新 build 树形结构(contructTree方法),从叶子节点计算选中态\展示态等数据,到顶级节点(主要是出于多选状态的计算考虑而设计);取用状态值时候,所有节点均被计算
  4. 受控场景,当 treeData 中有 value 时,需要做 value 到 key 的映射,在聚合选中项目时候,需要做遍历去重,当前两个场景下的耗时均为 O(n)

解决方案

  1. 使用拍平的列表 list 结构替换树形嵌套结构;只将页面展示的节点渲染为 TreeNode
  2. 增加虚拟化支持
  3. 所有数据存成 key-value 的对象,将所有状态存在 state 里,每次计算更新对应状态的 key ;只计算相关节点
  4. 使用 hashMap 完成 value 到 key 映射, set 完成去重操作, 时间复杂度从 O(n) -> O(1)

效果

  1. 单选挂载,花费时间与一级节点level=0的个数相关性更强,与总节点数关系更弱,与总级数的关系也较弱,整体时长降低 90%左右
  2. 搜索命中/全部展开,随着节点增多,scripting 耗时相比之前降低 70%左右
  3. 多选展开及选择 scripting 耗时相比之前降低 70%及以上
  4. 开启虚拟化,搜索命中/全部展开,1.1万节点与优化前相比,scripting 与 rendering 降低约96%
  5. 受控且 treeData 中存在 value ,全选 9w7 的场景从 35s 优化至 0.5s

Typography

问题场景

Typography 对 文本进行省略时,有两种方式。当设置中间截断(pos='middle')、可展开(expandable)、有后缀(suffix 非空)、可复制(copyable),启用 JS 截断策略;非以上场景,启用 CSS 截断策略。每当页面大小发生变化时,需要重新计算省略,设置 dom 省略后的内容。

原因

当页面中有多个 Typography ,或者页面中有 js 进程忙,此时页面就会卡顿。

解决方案

  • CSS Ellipsis 时,原先在渲染后的 didMount 生命周期计算并挂载 Popover的行为,修改为在用户鼠标 Hover 在该 tooltip 时再计算,避免首次挂载后多次计算和更新状态占用 JS 进程。
  • CSS Ellipsis 时,Typography 及其祖先 Resize 时不再根据 Overflow 计算是否展示 Tooltip,而在用户鼠标 Hover 时计算。
  • CSS 和 JS Ellipsis 时,不再监听 Typography 和 祖先元素的 Height 变化。现在 Typography 只对 Width 变化进行响应计算。
  • 修改 ResizeObserver 生效时机,并包装计算逻辑为异步,在首次计算完毕后设置文字缩略后才激活 ResizeObserver 回调,避免 didMount 后 JS Ellipsis 计算结果的更新导致触发 ResizeObserver 的二次无用计算。

效果

渲染次数测试

对于 1000 个 Typography

时间测试

对于 storybook中的 Typography 页面输出检测报告 (报告为下文性能检测工具输出)

  • URL: /iframe.html?args=&id=typography--ellipsis-single
函数名称耗时(ms)执行次数
renderEllipsisText↓-9.2 (13.2 => 4.0)16 次
ReactResizeObserver ReactResizeObserver.observeElement↓-6.1 (20.3 => 14.2)16 次
Base↓-12.7(13 => 0.3)9 次
Base.componentDidMount↓-6.9 (29.9 => 23)1次
显示第 1 条-第 4 条,共 4 条
  • URL: /iframe.html?args=&id=typography--edge-cases
函数名称耗时(ms)执行次数
Base.componentDidMount↑10.4 (0.2 => 10.6)3 次
显示第 1 条-第 1 条,共 1 条
  • URL: /iframe.html?args=&id=typography--ellipsis-collapsible
函数名称耗时(ms)执行次数
Base↓-8.7 (9.0 => 0.3)5 次
Base.renderEllipsisText↓-5.600002 (9.0 => 3.4)18 次
Base.componentDidMount↓-3.4 (22.5 => 19.1)5 次
ReactResizeObserver.observeElement↓-10.2 (26.4 => 16.2)18次
显示第 1 条-第 4 条,共 4 条
  • URL: /iframe.html?args=&id=typography--ellipsis-from-center
函数名称耗时(ms)执行次数
Base.renderEllipsisText↓-6.3(14.3 => 8.0)24次
显示第 1 条-第 1 条,共 1 条
  • URL: /iframe.html?args=&id=typography--ellipsis-chaos

带有事件监听逻辑的组件

目前Semi 组件监听器均为组件自己负责注册清除,如果出现 bug 或遗漏,组件在卸载后没有正确清除监听器会导致持有的 object 、dom 以及 react fiber 节点一直驻留内存。

解决方案

基于发布订阅模式的事件委托

  • 目前所有组件均继承于 BaseComponent,通过在 BaseComponent 基类对相同 dom 统一注册一次监听器,各个组件采用发布订阅方式将事件回调注册进 BaseComponent,BaseComponent 持有回调的 WeakRef。

基于 WeakRef 的自动化清理

  • 当组件被卸载,因为回调函数在 BaseComponent 中只存有弱引用,不影响回调函数被垃圾回收,当监听器回调被清除,回调持有的内存即被自动释放。当BaseComponent 已注册回调全被垃圾回收后,清除监听器。

效果

  • 组件内不再需要关心事件是否被清除,组件卸载后即自动清除监听器
  • 多个组件和实例共享一个监听器(例如 To B 页面大量弹层组件在 body 上的 clickoutSide 监听器),理论上相对于多个监听器,内存占用会更低

常态化监测

在业务迭代过程中,随着人员和需求的不断变化,我们需要一个自动化的方式去追踪每次代码变更对性能造成的影响,确保在正常迭代的过程中,任何对线上代码的变更不会导致非预期的性能体验下降。但目前业内普遍的性能检测均需要一定程度的人工参与,一些其他自动化工具过于偏向对已有页面的基础指标(文件大小,加载速度,渲染速度等),不仅不能确切圈定检测范围,在发生了性能劣化后也无法快速追踪到具体代码片段。 因此,我们根据需求,设计方案,开发并部署了常态化性能检测工具,用于对 PR 进行性能自动化追踪,防劣化的同时也能保证性能优化效果。

目的

  • 在项目迭代和维护的过程中,在测试机器上追踪前端网页性能,防止新代码发布后页面性能(执行时间,内存占用)发生预期外的劣化。
  • 研发人员可以手动对单个组件运行性能测试,测试不同写法对性能影响,追踪耗时具体代码行数,指导开发工作进行。

原理

  1. 在测试机器上对组件代码进行打包,通过 Babel 插桩埋点,记录每个函数的 耗时 和 内存
  2. 通过对每个组件的 StoryBook 页面进行用户常见操作的录制,通过 Headless 浏览器回放操作,对比不同 commit 之间函数执行后,资源( 耗时 和 内存 )的消费。
  3. 通过 CICD流程,每次 PR 合并后自动化检测变更的组件,将检测结果与上次对比,通过即时通讯工具通知到对应研发

方案

  • 录制交互操作(准备阶段,只需执行一次)
    1. 在 storybook 页面,录制用户在实际业务场景可能会对组件的交互操作,如点击,hover,展开,输入,滚动等,并存储入库。
  • 性能检测(每次分析代码性能时自动化执行)
    1. Clone Semi 仓库
    2. 安装依赖和 自定义 Babel 插件。
    1. git checkout 到对应 commit A B,编译 Storybook 页面
    2. 对 storybookA 和 storybookB 页面启动 HTTP 静态服务
    3. 比较 commitA 和 commitB 之间的代码,得到哪些组件的代码在 commit 之间发生变更
    4. Headless Chrome 访问 变更组件对应的 storybook 页面,通过 Devtool Protocal,回放用户在该页面操作
    5. 收集组件库每个函数内 Babel 插件插入的代码所记录的信息。将两个 commit 所收集的信息记录为 recordA 和 recordB。每个 record 内均有多个函数执行记录。
    6. 分析数据
      1. 分别根据每个 record 内记录的 函数 Raw 代码字符串,对字符串进行向量降维,计算对应函数的局部敏感哈希
      2. 交叉比较 recordA 和 recordB 内,每个函数记录的局部哈希的汉明距离,共比较 len(recordA) * len(recordB) 次。对 recordB 内每个函数记录均找到其在 recordA 内的最小汉明距离的函数记录,作为一对记录。并保存为 Map。
  1. 输出报告。比较每对记录中的两个函数记录的内存和执行时间变化,输出性能变化报告输出报告。比较每对记录中的两个函数记录的内存和执行时间变化,输出性能变化报告
  • 其他需要注意的点
    1. babel 插入的代码不能使用 eval 插入代码需要提前利用 babel generate 转换为 AST 插入,不能为了简便使用一行 eval 的 AST 直接插入代码字符串让浏览器自己在运行时eval。否则会在浏览器编译 JS 到 bytecode 时击穿 JIT,导致性能劣化严重,和实际不符。

    2. 获取函数名和函数体需要考虑多种 Case

      • 普通函数定义 function name(){}
      • 变量声明 const name = function(){}
      • 箭头函数 const name = ()=>{}
      • 对象属性声明 const obj = { name : function(){}}
      • 对象属性箭头函数声明 const obj = {name:()=>{}}
      • Class 声明 class MyClass{ name(){} }
      • Class 箭头函数声明 class MyClass {name = ()=>{}} 另外,因为 Semi 是 typescript 库,获取函数 body 时,如果函数在源码中不存在,而是通过前置 loader 插入的,比如 typescript 语法转义 es6 时的辅助函数,这些函数的 ast node 中的 loc (即位置对象)会不存在,需要单独过滤。
    3. 获取函数调用堆栈需要保留 sourcemap 利用 new Error 对象的方式获取堆栈字符串,用于显示在最终报告中,方便开发者回溯哪几行代码变更导致性能变化。storybook 在进行打包时,需要保留 sourcemap,方便新建 Error 获取堆栈时,能够拿到原始代码行数信息,也更易读。

    4. simhash 计算应放到浏览器外 不能放在浏览器端,会影响性能检测,且 js 作为解释型语言计算较慢。

    5. 从 浏览器 dump 数据到外部时应当注意切片 对于 Semi 复杂的前端代码,虽然外部计算逻辑因为编程语言的不同可以处理长数据场景,但数据在通过 devtools protocal 从浏览器到外部传输时序列化后字符串长度会超过 JS 的最大限制,需要分段获取。

实战示例

除去上文中 Typography Ellipsis 的优化使用了检测工具进行了验证外,我们也针对 PR #1854 进行了验证,

验证分为正向验证和反向验证,正向即用优化前代码比较优化后,反向用优化后比较优化前,从检测结果输出的函数路径和其执行时间的变化对比,可以看到经过组件代码中, 被 PR 修改的函数执行总时间有显著降低。

正向验证

普通量级

可以看到,在该量级(第一层 10 层)下,内存

内存变化: ↓-4584468.000000 (5017908.000000 => 433440.000000) 单位: 字节

执行时间变化: ↑5.800000 , 共执行 1 次 单位 ms

URL: /iframe.html?args=&id=tree--change-tree-data&viewMode=story 
 
函数名: getDerivedStateFromProps getDerivedStateFromProps , 内存变化: ↓-4584468.000000 (5017908.000000 => 433440.000000), 执行时间变化: ↑5.800000 (15.100000 => 20.900000) , 共执行 1 次 
堆栈: 

webpack://semi/packages/semi-ui/tree/index.tsx: 行10 列56  index.tsx 
webpack://semi/packages/semi-ui/tree/index.tsx
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行131 列303  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行179 列1  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行263 列420  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行246 列259  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行246 列191  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行239 列172  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行123 列110  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/scheduler/cjs/scheduler.production.min.js: 行19 列460  scheduler.production.min.js webpack://semi/node_modules/scheduler/cjs/scheduler.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行122 列318  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js 
 

扩大量级

扩大测试页面数量量级,第一层从 10 调整为 40 层,可以看到,在该量级(第一层 40 层)下,内存

内存变化: ↑7204484.000000 (1830404.00 => 9034888.00)

执行时间变化: ↑23.100000 (39.20 => 62.30) , 共执行 1 次

URL: /iframe.html?args=&id=tree--change-tree-data&viewMode=story 
 
函数名: getDerivedStateFromProps getDerivedStateFromProps , 内存变化: ↑7204484.000000 (1830404.000000 => 9034888.000000), 执行时间变化: ↑23.100000 (39.200000 => 62.300000) , 共执行 1 次 
堆栈: 

webpack://semi/packages/semi-ui/tree/index.tsx: 行10 列56  index.tsx 
webpack://semi/packages/semi-ui/tree/index.tsx
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行131 列303  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行179 列1  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行263 列420  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行246 列259  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行246 列191  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行239 列172  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行123 列110  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/scheduler/cjs/scheduler.production.min.js: 行19 列460  scheduler.production.min.js webpack://semi/node_modules/scheduler/cjs/scheduler.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行122 列318  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js 
 

反向验证

调换验证和基准的 commit,用优化后去比较优化前

./main --mode=vcs --version1=a1d1cdc1394b95 --version2=25f1fbd2b --component=tree--change-tree-data

普通量级

内存变化: ↑234676.00 (441080.00 => 675756.00) 执行时间变化: ↓-3.30(22.00 => 18.70) , 共执行 1 次

URL: /iframe.html?args=&id=tree--change-tree-data&viewMode=story 
 
函数名: getDerivedStateFromProps getDerivedStateFromProps , 内存变化: ↑234676.000000 (441080.000000 => 675756.000000), 执行时间变化: ↓-3.300000 (22.000000 => 18.700000) , 共执行 1 次 
堆栈: 

webpack://semi/packages/semi-ui/tree/index.tsx: 行10 列56  index.tsx 
webpack://semi/packages/semi-ui/tree/index.tsx
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行131 列303  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行179 列1  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行263 列420  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行246 列259  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行246 列191  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行239 列172  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行123 列110  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/scheduler/cjs/scheduler.production.min.js: 行19 列460  scheduler.production.min.js webpack://semi/node_modules/scheduler/cjs/scheduler.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行122 列318  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js 
 

扩大量级

内存变化: ↓-6929012.00 (9115548.00 => 2186536.00) 执行时间变化: ↓-22.70 (58.10 => 35.40) , 共执行 1 次

URL: /iframe.html?args=&id=tree--change-tree-data&viewMode=story 
 


函数名: getDerivedStateFromProps getDerivedStateFromProps , 内存变化: ↓-6929012.000000 (9115548.000000 => 2186536.000000), 执行时间变化: ↓-22.700000 (58.100000 => 35.400000) , 共执行 1 次 
堆栈: 

webpack://semi/packages/semi-ui/tree/index.tsx: 行10 列56  index.tsx 
webpack://semi/packages/semi-ui/tree/index.tsx
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行131 列303  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行179 列1  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行263 列420  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行246 列259  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行246 列191  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行239 列172  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行123 列110  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js
webpack://semi/node_modules/scheduler/cjs/scheduler.production.min.js: 行19 列460  scheduler.production.min.js webpack://semi/node_modules/scheduler/cjs/scheduler.production.min.js
webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js: 行122 列318  react-dom.production.min.js webpack://semi/node_modules/react-dom/cjs/react-dom.production.min.js 

优势

对于 commit 变更后和变更前的对比,工具能够观察到在 commit 中被修改的函数的执行时间的变化

  • 通过观察变化和相应堆栈上下文,能够帮助开发者及时回顾代码变更查看是否符合预期,防止合并到主分支并对用户页面造成性能劣化。
  • 同时,工具可以对组件性能的执行效率进行量化,便于各种代码优化的事后回归与统计,量化优化的程度。

不足

对于内存的变化,呈现比较随机。因为这里的内存使用的是 performance.memory.usedJSHeapSize,其统计的是函数执行前后的 JS Heap 大小,而浏览器的垃圾回收执行时间不可知,猜测可能在函数执行时,对内部已经使用完毕的变量即时地进行了回收,造成了函数内存检测最终结果的随机差异性。

总结

性能的影响指标涵盖很多方面,除了基础 lib 自身的代码外,也会与实际业务逻辑强相关,要保证性能基线,需要lib的维护者与具体业务系统的开发者协同。在用户的整个网页浏览过程中,性能不是最重要的,但是却深刻影响了用户体验与心情,也间接影响着网页的交互效率和留存率等重要指标,我们希望在未来不断提供功能和性能双双满意的组件库和配套基建,继续作为成为业务开发者在快速搭建产品原型时的趁手工具,提升每一个人(研发、设计与用户)的工作效率与幸福感。