精读《React 性能调试》

5,154 阅读5分钟

1 引言

在数据中台做 BI 工具经常面对海量数据的渲染处理,除了组件本身性能优化之外,经常要排查整体页面性能瓶颈点,尤其是维护一些性能做得并不好的旧代码时。

React 性能调试是面对这种问题的必修课,借助 Profiling React.js Performance 这篇文章一起学习一下这个技能吧。

2 精读

本文介绍了众多性能检测工具与方法。

React Profiler

Profiler 这个 API 是一种运行时 Debug 的补充,可以通过其 callback 拿到组件渲染信息,用法如下:

const Movies = ({ movies, addToQueue }) => (
  <React.Profiler id="Movies" onRender={callback}>
    <div />
  </React.Profiler>
);

function callback(
  id,
  phase,
  actualTime,
  baseTime,
  startTime,
  commitTime,
  interactions
) {}

这个 callback 会在每次渲染时执行,渲染分为初始化和更新阶段,通过 phase 区分,下面是参数详细说明:

  • id: 传入的 id。
  • phase: "mount" 或 "update",表示更新状态。
  • actualDuration: 实际渲染耗时。
  • baseDuration: 没有使用 memo 时的渲染预计耗时。
  • startTime: 开始渲染的时间。
  • commitTime: React 提交更新的时间
  • interactions: 何种原因导致的渲染,比如 setState 或 hooks changed 之类。

注意尽量不要轻易使用 Profiler 检测性能,因为 Profiler 本身也会消耗性能。

如果不想获得这么详细的渲染耗时,或者不想提前在代码中埋点,可以利用 DevTools 的 Profiler 查看更直观更简洁的渲染耗时:

其中 Ranked 可以展示按照渲染耗时排序后的结果,Interations 需要配合 Tracing API 使用,在后面会提到。

Tracing API

利用 scheduler/tracing 提供的 trace API,我们可以记录某个动作的耗时,比如 “点击添加按钮收藏一个电影” 耗时多久:

import { render } from "react-dom";
import { unstable_trace as trace } from "scheduler/tracing";

class MyComponent extends Component {
  addMovieButtonClick = (event) => {
    trace("Add To Movies Queue click", performance.now(), () => {
      this.setState({ itemAddedToQueue: true });
    });
  };
}

在 Interations 中可以看到动作触发的耗时:

这个动作还可以是渲染,比如可以记录 ReactDOM 渲染的耗时:

import { unstable_trace as trace } from "scheduler/tracing";

trace("initial render", performance.now(), () => {
  ReactDom.render(<App />, document.getElementById("app"));
});

甚至还可以追踪异步的耗时:

import {
  unstable_trace as trace,
  unstable_wrap as wrap,
} from "scheduler/tracing";

trace("Some event", performance.now(), () => {
  setTimeout(
    wrap(() => {
      // 异步操作
    })
  );
});

有了 Profilertrace 这两件武器,我们可以监控任意元素的渲染耗时与交互耗时,几乎可以涵盖所有性能监控需要。

Puppeteer

我们还可以利用 Puppeteer 实现自动化操作并打印报告:

const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const navigationPromise = page.waitForNavigation();
  await page.goto("https://react-movies-queue.glitch.me/");
  await page.setViewport({ width: 1276, height: 689 });
  await navigationPromise;

  const addMovieToQueueBtn =
    "li:nth-child(3) > .card > .card__info > div > .button";
  await page.waitForSelector(addMovieToQueueBtn);

  // Begin profiling...
  await page.tracing.start({ path: "profile.json" });
  // Click the button
  await page.click(addMovieToQueueBtn);
  // Stop profliling
  await page.tracing.stop();

  await browser.close();
})();

首先利用 puppeteer 创建一个浏览器,新建一个页面并打开 https://react-movies-queue.glitch.me/ 这个 URL,等待页面加载完毕后利用 DOM 选择器找到按钮,利用 page.click API 模拟点击这个按钮,并在前后利用 page.tracing 记录性能变化,并将这个文件上传到 DevTools Performance 面板,就会得到一份自动的性能检测报告:

这张图相当重要,是浏览器综合运行开销分析的利器,最上面分为 4 个部分:

  • FPS:每秒帧数,绿色竖线越高表示 FPS 越高,出现红线则表示出现了卡顿。
  • CPU:CPU 资源,用面积图展示消耗 CPU 资源的事件。
  • NET:网络消耗,每条横杠表示一种资源的加载。
  • HEAP:内存水位,由于短时间内看不出来是否会内存溢出,一般只用来简单看看内存消耗是否符合预期,对于内存溢出的检测需要用持续监控上报的方式。

下面会有一张 Network 详细图解,比如这张图:

细线表示等待的时间,粗线表示实际加载的情况,其中浅色部分表示服务器等待时间,即从发送下载请求到服务器响应第一个字节的时间。这部分可以看出资源并行加载阻塞情况以及资源服务器响应时间是否存在问题。

Timings 展示了几个重要时间节点,这里列举一部分:

  • FP:First Paint,第一次绘制。
  • FCP:First Contentful Paint,第一次内容绘制。
  • LCP:Largest Contentful Paint,最大内容绘制。
  • DCL:Document Content Loaded,DOM 内容加载完毕。

再下面是 JS 计算消耗,用了一张火焰图,火焰图是性能分析的常用可视化工具。以下面这张图为例:

看火焰图首先看跨度最长的函数,也就是最长的那条线,这是最耗时的部分,从左到右是浏览器脚本的调用顺序,从上到下是函数嵌套的顺序。

我们可以看到鼠标位置的 34 这个函数虽然长,但并不是性能瓶颈,因为下面执行的 n 函数长度和它一样,表示 34 函数的性能几乎无损耗,其性能由其调用的 n 函数决定。

我们可以利用这种方式一步步排查到叶子结点,找到对性能影响最大的元子函数。

User Timing API

我们还可以利用 performance.mark 自定义性能检测节点:

// Record the time before running a task
performance.mark("Movies:updateStart");
// Do some work

// Record the time after running a task
performance.mark("Movies:updateEnd");

// Measure the difference between the start and end of the task
performance.measure("moviesRender", "Movies:updateStart", "Movies:updateEnd");

这些节点可以在上面介绍的 Performance 面板中展示出来用于自定义分析。

3 总结

利用 Performance 进行通用性能分析,利用 React Profiler 进行 React 定制性能分析,这两个结合在一起几乎可以完成任何性能检测。

一般来说,首先应该用 React Profiler 进行 React 层面的问题筛查,这样更直观,更容易定位问题。如果某些问题跳出了 React 框架范围,或者不再能以组件粒度进行度量,我们可以回到 Performance 面板进行通用性能分析。

讨论地址是:精读《React 性能调试》 · Issue #247 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证