Skip to content

性能剖析与渲染预算 ​

问题:一次 18ms 的回调能证明页面慢吗 ​

不能。设备、开发模式、后台任务、热身和输入规模都会改变测量。<Profiler> 给出某次 React commit 的渲染成本,不包含网络等待,也不等于用户输入到下一帧的完整延迟。预算必须先固定场景,再收集多次样本。

完整浏览器实验 ​

将下面保存为 src/App.jsx,在已有 Vite React 19 项目运行。例子故意做 CPU 工作,只用于制造可见差异。

jsx
import { Profiler, useMemo, useState } from 'react'

const samples = []

function recordCommit(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  const sample = { id, phase, actualDuration, baseDuration, startTime, commitTime }
  samples.push(sample)
  if (samples.length > 100) samples.shift()
  console.table(samples.slice(-8))
}

function SlowList({ count, filter }) {
  const rows = useMemo(() => {
    const next = []
    for (let index = 0; index < count; index += 1) {
      let score = 0
      for (let spin = 0; spin < 2_000; spin += 1) score += Math.sqrt(index + spin)
      const label = `条目 ${index} / ${Math.round(score)}`
      if (label.includes(filter)) next.push(<li key={index}>{label}</li>)
    }
    return next
  }, [count, filter])
  return <ul>{rows}</ul>
}

export default function App() {
  const [count, setCount] = useState(300)
  const [filter, setFilter] = useState('')
  const [clicks, setClicks] = useState(0)

  return (
    <main>
      <h1>渲染测量台</h1>
      <button onClick={() => setClicks(value => value + 1)}>无关更新 {clicks}</button>
      <button onClick={() => { samples.length = 0 }}>清空采样</button>
      <label>条目数
        <input
          type="range"
          min="100"
          max="1200"
          step="100"
          value={count}
          onChange={event => setCount(Number(event.target.value))}
        />
      </label>
      <label>筛选
        <input value={filter} onChange={event => setFilter(event.target.value)} />
      </label>
      <Profiler id="results" onRender={recordCommit}>
        <SlowList count={count} filter={filter} />
      </Profiler>
    </main>
  )
}

打开控制台,先点“清空采样”,固定条目数,按同样节奏输入 2、清空,重复五轮。再改变条目数重做。不要同时改输入规模和代码,否则无法判断原因。只保留最近 100 条记录,避免测量器本身无限积累数据。这个小实验不启用 React Compiler,以便手动记忆化的对照条件明确。

一次只改变一个条件 ​

先不改变筛选条件,连续点“无关更新”。App 的点击数改变,SlowList 仍会被调用,但 count 和 filter 未变,useMemo 复用先前生成的行元素,昂贵循环不用重算。观察这些更新的 actualDuration,再与改变筛选条件的记录比较;后者依赖已变,需要重新计算。

第二组实验把 const rows = useMemo(() => { ... }, [count, filter]) 暂时改成 const rows = (() => { ... })(),其他部分不动。重复相同的无关更新。此时循环每次执行,才形成“依赖相同,是否复用计算”的有效对照。对照结束恢复 useMemo。

第三组只把 SlowList 用 memo 包装,保持 props 不变,再测无关更新。这次连子组件的函数调用也有机会跳过;但输入新筛选词时,filter 变化,依然要进入子组件。复用计算、跳过组件、降低更新优先级解决的是不同问题,不能用一个总体耗时猜测三者是否都生效。

如果没有明显差别,先检查测试规模和采样条件,不为证明优化“有效”而挑最快的一次。这个程序的瓶颈还包括创建与更新许多 DOM 节点;缓存只覆盖其中一部分。

读懂一次 commit ​

  • phase 区分首次 mount、普通 update 和嵌套更新。
  • actualDuration 是这次提交中 Profiler 子树实际花在渲染上的时间。
  • baseDuration 估算该子树不利用记忆化时的最近完整成本;actualDuration 明显低于它,才说明跳过工作可能有效。
  • 同一 commit 内的 Profiler 共享 commitTime,可以把多个子树的样本归组。

useMemo 只在依赖不变时复用计算。本例每次改变 filter 都必须重算,所以给 SlowList 再包 memo 并不会消除这项工作。真正的优化可能是改变产品交互(延迟非紧急筛选)、减少行数(虚拟化)或把检索移到索引/服务端,而不是无限添加 memo。

不能把 commitTime - startTime 当作 actualDuration 的替代。前者是两个时间点的墙钟间隔,期间可能发生等待或其他工作;后者关注 React 在本子树中实际记录的渲染工作。baseDuration 也不是每轮重新执行一遍“未优化版本”得出的实测数,而是用组件最近的渲染成本估算。要证明优化前后差异,仍需上面的真实对照。

从样本到预算 ​

先定义场景:“300 条数据、目标笔记本、连续输入 5 次、热身 1 轮后记录 5 轮”。再分别记录中位数和较慢样本,而不是把一次最快值当结论。16ms 可以作为一帧的粗略提醒,但不是通用通过线:浏览器还要执行事件、布局、绘制和其他脚本,不会把整帧都留给 React。

开发模式含额外检查,且 Profiler 自身有开销。React 的普通生产构建默认禁用程序化 profiling;若要采集生产等价数据,需要显式使用 profiling 构建,并评估采样成本。日常定位也可使用 React Developer Tools 的 Profiler。

React Compiler 改变哪一部分 ​

React Compiler 是构建时的自动记忆化工具,安装 React 运行时并不等于已启用它。第三章学过的引用关系仍然有用:它解释什么结果可以在依赖相同时复用,而编译器负责为可分析的组件和 Hook 自动安排这种复用。

例如父组件只改变主题时,列表数据没有变化,编译器可能复用列表计算和 JSX;用户真正修改筛选条件时,计算仍要执行。它不会把耗时算法自动变成 Worker,也不会替你解决网络缓存失效。违反纯渲染规则、在原对象上修改数据的写法,应先修正行为,不能期待编译器补救。

已有项目接入时,先保存本节的行为与性能基线,再按工具链启用编译,并比较同一组交互。不要一次删除全项目的手动记忆化后再猜差异来源。编译器安装与构建配置属于工具链分支;本节的测量实验在未启用它时即可完成。

边界 ​

  • Profiler 不能测网络 p95、服务端响应、布局偏移或图片解码;使用对应平台指标。
  • 时间断言在共享 CI 上容易波动。CI 更适合测试纯聚合逻辑、渲染次数上限或特定回归场景,把真实时间趋势留给受控基准与线上指标。
  • 优化前先保存行为测试。更快地渲染错误结果不是成功。

练习:汇总 commit 样本 ​

实现 summarize(samples):拒绝空数组或含负 actualDuration 的输入;返回 count、按从小到大排序后的中位数 median、向上取整位置的 p95,以及 maxBaseDuration。不得修改输入数组。用确定数据断言奇数、偶数、p95、非法值和输入顺序未变。

核对第 02 篇答案

下一篇 ​

发布、监控与升级会把测试和性能证据接到发布身份与回滚协议。

用心记录每一步成长