从性能优化发现了“复杂度守恒定律”

本文诞生于团队的一次性能优化项目,旨在记录经验为后来人提供参考,也待在将来知识体系有所更新时重新审视。

1. 背景与目标

团队开发的系统在上线后收到了一些流畅性方面的反馈。主观方面,有部分用户笼统地说“整体不流畅”,也有用户指出了某些步骤需要等待很久。客观方面,通过性能监控发现存在一定的卡顿现象,且发生节点能够和用户反馈频繁的位置对应。由此,系统的流畅性无论是在主观体验还是客观数据上都存在较大优化空间,所以团队启动了该系统的流畅性优化的项目。

2. 项目历程

优化项目从最初的指标确立到最后的线上验证,历时约3-4个月。项目可划分为如下五个阶段,每一步都建立在前一步数据的可信性上。

本文以一个典型的指标为例,从技术方面详细介绍优化流程和具体方法、对比各方法的适用场景和优劣势,为后续的技术选型提供参考。

3. 理论与实践

3.1 性能指标

3.1.1 通用指标

业界衡量Web性能的指标有很多,包括W3C performance工作组定义的从加载到交互的一系列指标、google发起的web vitals计划 定义的流畅性指标等。在此,我们根据需要优化的功能点,挑选了一些尽可能与用户体感接近的指标作为优化前期的测量指标。

3.1.1.1 INP

web.dev提出的Interaction to Next Paint (INP) 指标是一项待定的核心 Web 指标,(将)于 2024 年 3 月取代 First Input Delay (FID)。INP记录了从用户交互触发到页面下一次绘制的用时,其中交互可由 JavaScript、CSS、内置浏览器控件(如表单元素)或以上三者的组合驱动。根据web.dev给出的建议,INP值的75分位低于200ms则表明页面表现良好,在200ms-500ms之间表示需要改进,高于500ms则响应较慢。

3.1.1.2 响应延迟

google提出的RAIL模型中对用户对性能的感知给出了定量的描述(如下表)。其中,在 100 毫秒内完成由用户输入发起的转换,让用户感觉互动是瞬时完成的,超出这个阈值则视为一次响应延迟(Response)。

3.1.2 专用指标(自定义指标)

通用指标对开始和结束时间的定义总是以交互开始和下次绘制结束,而业务中链路不都是以下次绘制为交互的完成,也不完全是以用户交互为起点,所以,为了满足对业务链路的更精准的监控,我们自行实现了专用指标的埋点方法。
在参考了用户反馈和性能监控平台的响应延迟的节点之后,我们梳理了待优化的链路及其埋点位置,并使用performance API 中的mark & measure方法实现了自定义链路的用时采集,形成了若干个专用指标。

3.2 指标测量的方法和工具

在定义指标后,下一步需要测量当前系统各指标的水位值,在此过程中主要使用了如下工具。

3.2.1 Performance API

浏览器的Performance API提供了一组对各类性能指标的采集方法,包括可用于测量响应延迟的EventTiming API, 可用于检测长任务的LongTask API等等,除内置指标的测量,该API还支持自定义起点和终点的用时测量方法,即performance.mark & performance.measure,可帮助实现自定义指标的采集。

3.2.2 Chrome Dev Tools -- Performance

Chrome的开发者工具中的Performance工具可用于录制页面某段执行过程,除此之外,该工具提供了交互时间线、网络请求时间线、各线程调用栈、资源消耗图、CPU和网络节流状态模拟等一系列功能,可帮助开发者尽可能详细地定位性能瓶颈并比对优化效果。

3.2.3 性能检测平台(内部)的流畅性检测功能

我们所使用的性能监控平台设计了一组流畅性指标,并提供了相应的检测插件和数据监控平台。其中,响应延迟和卡顿两项指标可以帮助我们发现频繁发生延迟的节点。另外,结合其他类日志(如接口请求等),可以还原问题出现时的上下文,发现除交互链路本身之外的其他因素对交互链路响应时间的影响。

3.3 指标理想值

通用指标在业界存在着一些对理想值的共识,而专用指标则经过用户体验的实测,获取90分位数作为理想值。

3.3.1 通用指标

  • 用户交互响应时间:<= 100ms
  • 单个主线程任务用时:<= 50ms

3.3.2 专用指标

此处以优化的若干指标中的一个为例,先按照通用指标中的100ms设置理想值。埋点后发现指标当前的均值和P90值都已在100ms以内。大于P90值的数据虽然少,但在分布上呈长尾形态,且最大值已大于INP的500ms阈值来响应较慢的区间,带给用的体验较差。因此下一步的优化目标主要在于减少长尾数据,同时在整体的均值也会有所下降。
根据优化目标,设定检验优化效果的指标为延迟率,计算方法为用时超过理想值的次数 / 总次数,期望在优化后将延迟率减少至原来的一半。

3.4 性能瓶颈定位方法

3.4.1 performance录制和分析

Chrome的Performance工具除可测量指标外,更强大的功能在于能够帮助开发者分析各阶段用时和调用栈,从而定位性能瓶颈。当使用performance.mark API自定义指标时,可在interactions层看到指标对应的时间段,只需分析此阶段内的调用栈即可。从调用栈(通常也称为火焰图)来看,交互响应的时间可分为以下三个部分:

  • 排队时间(Input delay):从事件触发到回调开始被执行的用时
  • 回调执行时间(Interaction event callbacks):回调执行的用时
  • 渲染时间(Rendering work):回调执行结束到渲染完成的用时

排队时间和回调执行时间

渲染时间

(以上两张图片的来源:https://web.dev/articles/diagnose-slow-interactions-in-the-lab?hl=zh-cn)

可通过火焰图看出各部分时间的长短,耗时明显最长的部分则为性能瓶颈。对于不同部分,web.dev给出了不同的优化策略,如 优化排队时间优化回调执行时间优化渲染时间 等等。

观察指标的火焰图(如下),发现性能瓶颈明显在回调执行阶段,因此后续的分析和探索也主要集中在此阶段。

3.4.2 链路梳理

为便于将火焰图中的调用栈对应到代码,通过走查代码梳理了埋点位置的起点和终点之间所执行的方法。

较为意外的发现是,除内部链路外,在用户交互与系统的自动执行任务在临近的时间出现时,用户交互的响应延迟是单独出现时的4倍。自动执行任务是系统为保证数据的时效性而设计的数据自动更新任务,在系统数据变更的高峰时段,会频繁执行,与用户交互“相撞”(同时发生)的概率非常大。从全场景埋点也可以佐证这一点,即当有大量接口调用时,临近时间的交互用时会长很多。因此,除了优化链路内部的任务,我们还需要考虑上下文尤其是自动更新任务。

3.5 复杂度的定位

对复杂度的管理是Computer Science领域的一个核心问题,对于性能优化来说则更处于核心位置。对复杂度的分类有多个维度,在此以“内部 vs. 外部” “业务 vs. 技术” 两个维度进行分类和分析。

3.5.1 内部 vs. 外部 复杂度

内部复杂度即链路内所执行的任务的复杂度

外部复杂度指链路之外的方法且影响到了链路用时,主要包括系统的自动更新任务。

3.5.2 业务 vs. 技术 复杂度

Accidental Complexity vs. Essential Complexity
-- The mythical man-month

业务方面的复杂度由业务逻辑本身决定,主要有:

  • 数据呈现的样式较为复杂,判断分支较多、展示字段较多
  • 数据的自动更新与交互可能同时触发
  • 数据之间的依赖过多,影响到的组件过多
  • 链路中的联动位置较多

技术方面的复杂度由使用的技术栈决定,主要有:

  • react virtual dom render 相比直接操作dom增加了render层的成本
  • mobx ovservable update 相比不使用observable更加了数据维护成本
  • virtual list scroll 在列表中实现虚拟滚动,减少DOM数量

还有部分复杂度是业务和技术共同构成的,如数据之间的依赖过多 + 大量使用mobx计算属性以自动管理依赖。

3.6 复杂度的消解

对复杂度的消解,存在着一些最佳实践,如长任务拆分为短任务、延迟优先级较低的任务、将部分任务转移到其他线程等。此处结合所使用的技术栈能够提供的能力,具体列出项目中使用的消解方案。

3.6.1 mobx 使用方法的改变

3.6.1.1 computed 改 state,重获对更新时机的控制权

部分变量从自动更新的computed(计算属性)改为需要手动更新的state,虽然在写法和依赖管理上有所“倒退”,但是能够重新获得对各个变量更新时机的控制权,从而间接控制依赖各变量的组件更新的优先级。

3.6.1.2 deep 改 shallow,减少observable维护成本

注:以下以“A类数据”“B类数据”等名称指代系统内部的某类业务数据。

由于A类数据的数据结构比较复杂,全部使用deep类型的observable维护(即递归观察每一个字段),将会使得数据的更新时间较长,所以将复杂结构的 state 由 deep 类型改为 shallow 类型(只观察顶层引用),可减少观察负荷,缩短长任务的长度。
实验效果如下:能够将mobx层的用时减少了80%,改造成本几乎为零。难道遇到了所谓的“银弹”?


然而,从整个链路看,这部分复杂度并没有凭空消失,而是转移到了react层,使得render的成本有所增加,总的时长几乎没变。下图中调用栈顶部为XHR Load的是mobx层的方法,为Function Call的是react层的方法。改造前后的火焰图对比如下:
改造前:mobx层的方法执行时间更长,react层的方法用时较短。


改造后:mobx层的方法用时变短,但react层的用时边长。总用时没有明显减少。


这个结果是否意味着deep改shallow不能为系统带来优化?暂时不能下此结论,因为复杂度转移到react层后,还是有可能通过切分、延迟等策略消解的。

3.6.2 react 使用方法的改变,延迟部分更新

react从18版本开始提供了一些API供开发者告知react哪些任务可以被延迟执行,具体如下:

  • useDeferredValue:将组件内可延迟响应的 state 使用 useDeferredValue 包裹,实现改变量相关内容的延迟更新。
  • useTransition / startTransition:将 react 组件内可延迟执行的副作用用 useTransition 或 startTransition 包裹,实现部分逻辑的延迟。

    (图片来源:https://vercel.com/blog/how-react-18-improves-application-performance)

3.6.3 浏览器延迟类 API 的使用,延迟部分任务

  • requestIdleCallback:将任务安排为尽可能低的优先级,并且仅在浏览器空闲时执行。支持设置超时时间以防止任务永远得不到执行。可用于延迟后台任务的执行。
  • setTimeout:在指定时间延迟后将任务放入事件队列中,可用于作为requestIdleCallback的pollyfill

3.6.4 按用户关注点重排任务优先级

在原有链路中,用户关注的A类相关的数据的渲染处于链路中最后的位置,所以用户感受到交互有较大延迟。在将原有的计算属性改为state后,我们得以手动控制state的更新数据,A类数据可以独立甚至是优先于B类数据被更新,相应的组件渲染过程也会提前,使得从用户点击到A类呈现的时间有所缩短。

3.6.5 改动范围

优化过程独立对各API的使用进行了测试,由于mobx层的部分改动会导致react层的执行时间变长,所以react层也需要做相应的优化才可抵消掉转移到这一层的复杂度,抵消的方法则为使用一系列延迟类API将长任务进一步切分和消解。

4. 优化效果与复盘

4.1 优化效果

专用指标、通用指标均有较大变化,用户的主观反馈也提到流畅性有所提升。

4.2 优化方案复盘

本次优化所使用的方案按API分类为如下6个,其中绿色的标注表示优势,红色的表示劣势。黄色标注的表示需要有条件地使用,即待优化的问题与方案的适配度较高则可以从方案受益,否则该方案可能并不能达到优化的效果,甚至会带来一些其他问题。

5. 优化实践总结

性能优化的核心本质上是复杂度的定位与消解,所以本文所讨论的解决方案也主要由这样两个阶段组成:

5.1 复杂度的定位和分析

此过程旨在找到性能瓶颈。根据性能优化领域Amdal定律,优化某一部分能为系统带来的总体性能提升,取决于该部分在系统性能表现中所占的比例。因此,从复杂度高的、用时长的任务入手,逐步消解,则可以达到性能逐步提升的目的。
在此过程可以使用如下工具辅助:

  • Performance API
  • Chrome Dev Tool:Performance工具、Rendering工具、Performance Monitor工具、Lighthouse工具
  • ahooks useWhyDidYouUpdate

5.2 复杂度的消解

在找到复杂度后,将复杂度消解成为达成优化目的的核心步骤。常见的消解策略与最佳实践有:

  • 将长任务切分为短任务
  • 延迟执行优先级较低的任务
  • 将部分任务移到主线程外

对以上策略的实现与系统所用的技术栈有关,在此列举基于一些常用技术栈的可用API:

  • mobx: deep --> shallow, computed --> state
  • react (v18+):  useDeferredValue, useTransition, startTransition
  • 浏览器(non-safari):requestIdleCallback

参考资料

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值