《ReactJS性能剖析》(译)

原文地址。带补充标示的地方是翻译过程中拓展的知识点。

今天,我们来看看如何使用 ReactProfiler API 来测试 React 的渲染性能; 如何使用 React 实验性的 交互追踪 API 来追踪 React 的交互;如何使用 Timing API 来测量自定义指标。

为了方便演示,我们将使用一个展示电影列表的应用。

React Profiler API


React 提供的 Profiler API 用于测量 渲染和渲染成本,以帮助我们定位应用程序缓慢的瓶颈。

import React, { Fragment, unstable_Profiler as Profiler} from "react";

Profiler 使用 onRender 回调作为一个 prop,被分析的树中的组件每次提交更新时,这个回调都会被执行。

const Movies = ({ movies, addToQueue }) => (
  <Fragment>
    <Profiler id="Movies" onRender={callback}>

为了测试,让我们试着使用 Profiler 来测量部分 Movies 组件的渲染时间。像这样:

ProfileronRender 回调接收一些参数,用于描述渲染的内容和渲染时间。这些参数如下:

  • id: 提交更新的 Profiler 树的 “id“ 属性。
  • phase: “mount“ (首次加载) 或 “update“ (重现渲染)
  • actualDuration: 提交更新的渲染时间
  • baseDuration: 没有记忆化(memoization)的情况下,渲染所有子节点的估时
  • startTime: React 开始渲染的时间
  • commitTime: React 完成渲染的时间
  • interactions: 引发更新的具体交互

补充:Memoization 是一种将函数返回值缓存起来的空间换时间的方法。原理很简单,就是把函数的每次执行结果都放入一个键值对(数组也可以)中,在接下来的执行中,在键值对中如果有值,直接返回该值,没有才去执行函数体求值并缓存。现代 JavaScript 中经常使用这种技术。React useMemo就是通过 memoization 来提高性能的。


const callback = (id, phase, actualTime, baseTime, startTime, commitTime) => {
    console.log(`${id}'s ${phase} phase:`);
    console.log(`Actual time: ${actualTime}`);
    console.log(`Base time: ${baseTime}`);
    console.log(`Start time: ${startTime}`);
    console.log(`Commit time: ${commitTime}`);
}

我们可以加载我们的页面,前往 Chrome DevTools 控制台,应该可以看到以下时间:

我们也可以打开 React DevTools,进入 Profiler 标签,直观地看到我们的组件的渲染时间。下面是火焰图的视图:

我也很喜欢使用 Ranked 视图,它是按顺序排列的,所以渲染时间最长的组件会显示在最上面。

你也可以使用多个 Profilers 来测量你的应用程序的不同部分。


import React, { Fragment, unstable_Profiler as Profiler} from "react";

render(
  <App>
    <Profiler id="Header" onRender={callback}>
      <Header {...props} />
    </Profiler>
    <Profiler id="Movies" onRender={callback}>
      <Movies {...props} />
    </Profiler>
  </App>
);

补充: React devtoolsProfiler 功能 只支持 React v16.5+ 构建的应用的追踪。因为 React 16.5 添加了对开发者工具的 Profiler 插件的支持。

但是,如果你想进行交互追踪怎么办?

交互追踪 API


如果我们能够追踪交互(例如点击用户界面),以回答 “这个按钮的点击需要多长时间来更新DOM?”这样的问题,那将是非常强大的。感谢Brian VaughnReact 通过新的 scheduler包中的交互追踪 API 对交互追踪提供了实验性支持。这里有更详细的记录。

交互被注释为一个描述(例如 “点击添加到购物车按钮”)和一个时间戳。交互也应该提供一个回调,在那里你可以做与交互有关的工作。

在我们的 “Movies“ 应用程序中,我们有一个 “将电影添加到队列 “ 按钮(”+”)。点击这个按钮将电影添加到你的观看队列中。

下面是一个追踪这种交互的状态更新的例子。

import { unstable_Profiler as Profiler } from "react";
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 });
    });
  };
}

我们可以记录这种交互,并在 React DevTools 中看到它的持续时间:

我们也可以使用交互追踪API来追踪初始渲染,如下所示:

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

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

Brian在他的 React gist 中涵盖了更多的交互追踪的例子,比如如何追踪异步交互。

补充:Github 提供了一个非常有用的服务 Gist。开发人员可以使用 Gist 记录他们的代码片段,但是 Gist 不仅仅是为极客和码农开发的,每个人都可以用到它。

Puppeteer

对于更深入的 UI 交互脚本跟踪,您可能会对 Puppeteer 感兴趣。Puppeteer 是一个 Node 库,它提供了一系列 高级 API,用于通过 DevTools 协议控制 无头 Chrome 浏览器

它暴露了 trace.start()/stop() 助手,用于捕获 DevTools 的性能追踪情况。下面,我们使用它来跟踪单击主按钮时发生的情况:

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();
})()

加载 profile.jsonDevToolsPerformance 面板中,我们可以看到点击按钮后的所有 JavaScript 函数调用:

如果你有兴趣阅读更多关于这个主题的内容,请查看 Stoyan Stefanov 的文章 JavaScript组件级CPU成本

User Timing API


User Timing API允许使用 高精度时间戳 为应用程序度量自定义性能指标。Window.performance.mark() 存储具有关联名称的时间戳,而 window.performance.measure() 存储两个标记之间经过的时间。

// 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');

在使用 Chrome DevToolsPerformance 面板分析 React 应用程序时,你会发现一个名为 “Timings” 的部分,里面包含了你的 React 组件的处理时间。在渲染时,React 能够通过 User Timing API 发布该信息。

注意: React 正在从他们的 DEV 包中移除 User Timing,以支持 React Profiler,后者提供了更准确的计时。他们可能会在未来的3级浏览器中重新添加 User Timing

纵观整个 web,你会发现 React 应用利用 User Timing 来定义自己的定制指标。其中包括 Reddit 的“Time to first post title visible” 和 Spotify 的 “Time to playback ready“:

补充:Spotify 是世界上最大的音乐流媒体服务。Reddit是个社交新闻站点。

自定义的 User Timing 标记和度量也可以清晰的反映在 Chrome DevToolsLighthouse面板:

最近版本的 Next.js 还为一些事件添加了更多的 User Timing 标记和度量,包括:

  • Next.js-hydration:hydration 时间。
  • Next.js-nav-to-render:导航开始,直到呈现之前。

所有这些度量都将出现在 Timings 区域:

DevTools & Lighthouse


提醒一下, LighthouseDevTools Performance panel 面板 可用于深入分析 React 应用程序的加载和运行时性能, 突出关键以用户为中心的幸福指标:

React 用户可能会喜欢像Total Blocking Time (TBT)这样的新指标,它可以衡量一个页面从最初的不可响应变得具有可靠的响应性(Time to Interactive)的过程。下面我们可以看到使用 Concurrent 模式前后,TBT的情况:

补充:Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。

这些工具通常有助于获得浏览器级的瓶颈视图,如延迟响应的繁重长任务(如按钮单击响应),如下所示:

Lighthouse 还提供了一些 React 特定的的审记指引。在 Lighthouse 6.0 中,您将看到一个 remove unused JavaScript audit 的审记,高亮提示可以使用 React.lazy() 动态引入这些已加载但未使用的 JavaScript

这总比在真实用户的硬件上对性能进行体检要好。我经常依靠webpagetest.org/easy和来自RUMCrUX的现场数据来描绘一个更完整的画面。

Read more