# 如何分析和调优性能瓶颈?

4 个方面 RAIL 指响应(Response): 应在 50 毫秒内完成事件处理并反馈给用户 动画(Animation): 10 毫秒内生成一帧; 浏览器空闲时间(Idle): 最大化利用浏览器空闲时间; 加载(Load):在 5 秒内完成页面资源加载且使页面可交互。

  • 衡量工具

    • Lighthouse Chrome 浏览器的 Lighthouse 查看优化方向 但 Lighthouse 并不能真实地反映出每个用户的设备的实际性能数据; Lighthouse 的分数反应的是业界的标准,而非项目实际需求的标准。

    • 自行完成性能指标的采集 可以考虑使用网页 APM工具 例如:New Relic,国内使用阿里云的 ARMS

  • 采集指标

    • FCP(First Contentful Paint),首次绘制内容的耗时。首屏统计的方式一直在变,起初是通过记录 window.performance.timing 中的 domComplete 与 domLoading 的时间差来完成,但这并不具备交互意义,现在通常是记录初次加载并绘制内容的时间。
    • TTI(Time to Interact),是页面可交互的时间。通常通过记录 window.performance.timing 中的 domInteractive 与 fetchStart 的时间差来完成。
    • Page Load,页面完全加载时间。通常通过记录 window.performance.timing 中的 loadEventStart 与 fetchStart 的时间差来完成。
    • FPS,前端页面帧率。通常是在主线程打点完成记录。其原理是 requestAnimationFrame 会在页面重绘前被调用,而 FPS 就是计算两次之间的时间差。
    • 静态资源及API 请求成功率。通常是通过 window.performance.getEntries( ) 来获取相关信息。
  • 排查 定目标 在性能监控中有一个概念叫TP(Top Percentile),比如 TP50、TP90、TP99 和 TP999 等指标,指高于 50%、90%、99% 等百分线的情况。如 TP50 就意味着,50% 的用户打开页面绘制内容的时间不超过 6 秒,90%的用户不超过 8 秒。如果要提升 FCP,那么就需要提升 TP 50、TP90、TP999 下的数据,这才是有正确方向的目标。

  • 实施

    • FCP:加载一个 React 页面,通常是从白屏到直接显示内容。那么如果白屏时间很长,用户可能会流失,就需要在页面上绘制内容,给出一些反馈。 最早的优化方案是绘制一个Loading图标,写死在 HTML 的 CSS 里,等 JS 开始执行的时候再移除它。

    后来有了”骨架屏“的概念,在内容还没有就绪的时候,先通过渲染骨架填充页面,给予用户反馈。

    还有一种解决方案是SSR,也就是走服务端渲染路线,常用的方案有 next.js 等。

    • TTI:可以优先加载让用户关注的内容,让用户先用起来。策略上主要是将异步加载与懒加载相结合 如: 核心内容在 React 中同步加载; 非核心内容采取异步加载的方式延迟加载 内容中的图片采用懒加载的方式避免占用网络资源。

    • Page Load 页面完整加载时间同样可以通过异步加载的方式完成。异步加载主要由 Webpack 打包 common chunk 与异步组件的方式完成。

    • FPS FPS 主要代表了卡顿的情况,在 React 中引起卡顿的主要原因有长列表与重渲染。 "长列表的解决方案"很成熟,直接使用 react-virtualized 或者 react-window 就可以 重新渲染看👇 “如何避免重复渲染”

    • 静态资源及 API 请求成功率 静态资源及 API 请求成功率的统计是非常有意义的。两者都有可能出现在用户的机器上失败,但在自己的电脑上毫无问题的情况。导致这个问题的原因千奇百怪。 例如: 你是直接从前端服务器拉取 JS 与 CSS 资源,还是从 CDN 拉取的? 解析 CDN 与 API 域名存在失败的情况。 运营商对静态资源及 API 请求做了篡改,导致请求失败。

      解决: 对于静态资源而言,能用 CDN 就用 CDN,可以大幅提升静态资源的成功率。 如果域名解析失败,就可以采取静态资源域名自动切换的方案;还有一个简单的方案是直接寻求 SRE 的协助。 如果有运营商对内容做了篡改,我推荐使用 HTTPS。

    • 收益 “技术必须服务于业务”,否则就只是技术团队的自嗨。 所以从技术角度讲收益,需要从业务实际效益出发。就像开篇所说的:“如果一个移动端页面加载时长超过 3 秒,用户就会放弃而离开。”那么将 TP999 从 5 秒优化到 3 秒以内,就可以得出具体的用户转化率数据。这样的技术优化才是对公司有帮助的。

# 如何避免重复渲染

但需要注意优化分时机 “过早的优化是万恶之源” —— 出自 “计算机编程艺术”。 分析走以下方式答题:

  • 优化时机,说明应该在什么时候做优化,这样做的理由是什么;
  • 定位方式,用什么方式可以快速地定位相关问题;
  • 常见的坑,明确哪些常见的问题会被我们忽略,从而导致重渲染;
  • 处理方案,有哪些方案可以帮助我们解决这个问题。

# 原理 为什么需要优化

React 会构建并维护一套内部的虚拟 DOM 树,因为操作 DOM 相对操作 JavaScript 对象更慢,所以根据虚拟 DOM 树生成的差异更新真实 DOM。那么每当一个组件的 props 或者 state 发生变更时,React 都会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM。这个过程被称为协调。

协调的成本非常昂贵,如果一次性引发的重新渲染层级足够多、足够深,就会阻塞 UI 主线程的执行,造成卡顿,引起页面帧率下降。

# 答题

如何避免重复渲染分为三个步骤:选择优化时机、定位重复渲染的问题、引入解决方案。

优化时机需要根据当前业务标准与页面性能数据分析,来决定是否有必要。如果卡顿的情况在业务要求范围外,那确实没有必要做;如果有需要,那就进入下一步——定位。

定位问题首先需要复现问题,通常采用还原用户使用环境的方式进行复现,然后使用 Performance 与 React Profiler 工具进行分析,对照卡顿点与组件重复渲染次数及耗时排查性能问题。

通常的解决方案是加 PureComponent 或者使用 React.memo 等组件缓存 API,减少重新渲染。但错误的使用方式会使其完全无效,比如在 JSX 的属性中使用箭头函数,或者每次都生成新的对象,那基本就破防了。

针对这样的情况有三个解决方案:

  1. 缓存,通常使用 reselect 缓存函数执行结果,来避免产生新的对象;
  2. 不可变数据,使用数据 ImmutableJS 或者 immerjs 转换数据结构;
  3. 手动控制,自己实现 shouldComponentUpdate 函数,但这类方案一般不推荐,因为容易带来意想不到的 Bug,可以作为保底手段使用。