陌上人如玉
公子世无双

5.1 定位性能问题

5.1 定位性能问题

一句话:优化前必须先测量——用 React DevTools Profiler 找到「哪些组件渲染次数多、耗时长、为什么渲染」,再用浏览器 Performance 面板看非 React 的部分(布局、绘制、长任务)。

一、先分类:两类性能问题

① React 相关的(渲染层面)
   · 组件渲染次数过多(无谓的重渲染)
   · 单个组件渲染耗时长(大列表、复杂计算)
   · 渲染范围过大(父组件更新带动整棵子树)
   → 工具:React DevTools Profiler

② 非 React 相关的(浏览器层面)
   · 布局抖动(layout thrashing)
   · 大量 DOM 节点(长列表、深层嵌套)
   · 频繁的样式重算与绘制
   · 长任务阻塞主线程(不只 React 的渲染)
   · 网络与资源加载
   → 工具:浏览器 Performance 面板 / Lighthouse / Network 面板
⚠️ 关键前提:先明确「哪类问题」,再选工具。
   如果卡顿来自「网络慢」或「DOM 太多」,加 memo 完全没用。
症状 可能的原因 优先工具
输入框打字卡顿 昂贵更新阻塞(大列表过滤/渲染) Profiler + Performance
页面首次加载慢 包体积、请求瀑布、SSR 缺失 Lighthouse / Network
滚动掉帧 DOM 节点过多、滚动监听里的重计算 Performance(FPS 行)
切换 Tab 卡一下 一次性渲染大量内容 Profiler(看那次交互)
内存持续增长 未清理的订阅/定时器、缓存无上限 Memory 面板
某些操作偶尔卡 长任务(大数据量时才触发) Performance(长任务标记)

二、React DevTools Profiler

使用步骤:
① 安装 React Developer Tools(浏览器扩展)
② 打开「Profiler」标签
③ 点「录制」→ 在页面上操作 → 停止录制
④ 看结果:Flamegraph(火焰图)/ Ranked(排名)/ Timeline(时间线)
⑤ 选中某个组件,在右侧看「Why did this render?」(为什么渲染)
三个视图的读法:

① Flamegraph(火焰图)
   · 每个横条是一个组件的渲染
   · 宽度 = 这次渲染耗时(占比)
   · 颜色:灰色 = 本次未渲染;蓝/黄 = 渲染了(黄越深耗时越长)
   · 有「灰色但有边框」的组件 = 渲染了但耗时极短(可忽略)

② Ranked(排名)
   · 按「渲染耗时」从高到低排序
   · 快速定位最耗时的组件(优化优先级最高)

③ Timeline(时间线)
   · 横轴是时间,纵轴是组件树
   · 看「哪一次交互触发了哪些组件的渲染」
⭐ 「Why did this render?」是 Profiler 最有价值的功能:

它会告诉你每个组件重渲染的原因,常见的有:
· Props changed(哪些 props 变了)
· State changed
· Parent re-rendered          ← 最常见!
· Context changed
· Hooks changed
· First render

→ 「Parent re-rendered」+ props 没变 = 可以用 memo 优化

三、关键指标

看 Profiler 时关注三件事:

① 渲染【次数】
   · 一次交互里,同一个组件被渲染了几次?
   · 渲染 3 次以上通常说明有「连锁更新」(如 Effect 里 setState 引发二轮渲染)

② 渲染【耗时】
   · 单个组件耗时超过 1~2ms 就值得看
   · 累计耗时占整次交互的大部分 → 优化重点

③ 渲染【原因】
   · Parent re-rendered → 考虑 memo / children 透传 / 状态下移
   · Context changed → 拆分 Context / 换状态库
   · State changed → 检查是否必要(能否用 ref / 状态机)
// 快速「手工测量」的办法(临时加,不要提交)
function Component(props) {
  const renderCount = useRef(0);
  renderCount.current++;

  const start = performance.now();
  // ... 组件逻辑
  const duration = performance.now() - start;

  useEffect(() => {
    if (duration > 2) {
      console.warn(`[慢渲染] ${Component.name} 第 ${renderCount.current} 次,耗时 ${duration.toFixed(2)}ms`);
    }
  });

  return <div>...</div>;
}
// 更规范的做法:封装一个「渲染耗时监控」的 Hook(仅开发环境)
function useRenderProfiler(name: string) {
  const start = performance.now();
  const count = useRef(0);
  count.current++;

  useLayoutEffect(() => {
    const duration = performance.now() - start;
    if (process.env.NODE_ENV === 'development' && duration > 5) {
      console.warn(`[render] ${name} #${count.current} ${duration.toFixed(2)}ms`);
    }
  });
}

四、Highlight updates(最直观的功能)

React DevTools 的「Highlight updates when components render」:
① 在 DevTools 的「Components」标签里打开这个开关
② 页面上任何组件渲染都会【高亮边框】
③ 快速确认「哪个区域在频繁渲染」

使用建议:
· 输入一个字,看哪些区域闪 → 闪得多的就是渲染范围过大
· 如果「整页都在闪」→ 状态放太高了(参考 5.4 的状态下移)

五、浏览器 Performance 面板

录制步骤:
① 打开 Performance 面板 → 点录制 → 操作 → 停止
② 关注这几行:
   · Main(主线程):JS 执行、长任务(红色三角标记)
   · Timings:LCP / FCP 等标记
   · Frames:FPS(有红条说明掉帧)
   · Layout / Recalculate Style:布局与样式重算
   · Paint / Composite:绘制与合成
常见问题的特征:

① 长任务(Long Task,> 50ms)
   · 表现为 Main 行上有一段很宽的黄色/红色
   · 定位:看那段里的调用栈(可能是 React 渲染、也可能是业务计算)
   · 影响:期间用户输入无响应(INP 变差)

② 布局抖动(layout thrashing)
   · 表现为 Layout 行上有大量密集的小块
   · 原因:在循环里「读尺寸 → 改样式 → 再读尺寸」
   · 修复:批量读、批量写(读用 getBoundingClientRect 一次拿完)

③ 强制同步布局(Forced reflow)
   · 特征:JS 里读到 `offsetWidth` 后就出现 Layout
   · 修复:缓存尺寸、用 ResizeObserver、避免在循环里读

④ 大量 DOM 节点
   · 特征:Layout/Paint 耗时高,节点数(Performance 里可以看到 DOM 数量)
   · 修复:虚拟滚动(见 5.5)
// 布局抖动的典型反例与修复
// ❌ 每次循环都「读 → 写 → 读」
items.forEach((item) => {
  const li = document.createElement('li');
  li.style.height = `${container.offsetHeight}px`;   // ⚠️ 读(触发 layout)
  li.style.width = `${container.offsetWidth}px`;     // ⚠️ 又读
  fragment.appendChild(li);
});

// ✅ 先读一次(批量),再写
const { offsetHeight, offsetWidth } = container;      // ⭐ 一次读完
items.forEach((item) => {
  const li = document.createElement('li');
  li.style.height = `${offsetHeight}px`;
  li.style.width = `${offsetWidth}px`;
  fragment.appendChild(li);
});

六、优化决策流程

① 明确「哪个交互慢」
   用 Profiler 录制那个具体的操作(不要笼统地「优化性能」)

② 判断「是 React 渲染慢,还是别的东西慢」
   · Profiler 里那次交互总耗时 ≈ Performance 面板里的对应长任务 → React 相关
   · Profiler 里组件耗时都很小,但 Performance 里有长任务 → 非 React(业务计算/DOM)

③ 如果是 React 渲染慢,继续分:
   · 渲染【次数】多 → 减少重渲染(memo / children / 状态下移 / Context 拆分)
   · 单次渲染【耗时】长 → 拆分组件 / useMemo / 虚拟滚动 / 用 transition 降优先级

④ 如果是非 React:
   · DOM 节点过多 → 虚拟滚动
   · 布局抖动 → 批量读写
   · 业务计算重 → Web Worker / 惰性计算 / 缓存

⑤ 优化后【重新测量】,确认改善(并检查是否引入了新问题)
测量结果 优化手段 见
渲染次数多(Parent re-rendered) memo / children / 状态下移 5.2、5.4
单组件渲染耗时长 拆分、useMemo、虚拟滚动 5.3、5.5
Context changed 导致大量渲染 拆分 Context / 状态库 5.9
输入导致长任务 useDeferredValue / 防抖 5.7
首屏慢 代码分割、预加载 5.6
DOM 节点多 虚拟滚动 5.5

七、常见坑

① 不测量就优化(凭感觉加 memo)
   → 有可能「渲染次数减少了但耗时没变」,因为瓶颈在别处

② 用 production 构建测量
   → 开发构建有额外检查,数字会失真;但也别只看 production 而忽略开发体验

③ 只测一次(数据量、网络状况、设备不同结果差异很大)
   → 应该在「接近真实的设备与数据量」下测多次

④ 忽略「低频但极慢」的操作
   → 比如「打开一个弹窗卡 2 秒」,可能被平均指标掩盖

⑤ 只看「渲染次数」不看「耗时」
   → 渲染 10 次耗时 0.1ms 的组件,优化它毫无意义

⑥ 忽略非 React 的瓶颈(DOM 数量、布局抖动、网络)
⑦ 在 Profiler 里录制「包含大量无关操作」的长片段(应只录目标交互)
⑧ 用 console.log 统计渲染次数但忘了它本身有开销
⑨ 只看 Performance 面板却不知道「哪个 React 组件在渲染」(应该两个工具配合)
⑩ 优化后不重新测量
⑪ 用「平均值」掩盖「P95/P99 的卡顿」
⑫ 忘了「React DevTools 本身会拖慢性能」(测量时可以先关掉高亮等功能)

八、面试延伸

  1. 「你怎么定位 React 的性能问题?」
    分三步:① 明确症状与目标交互——是哪一次操作慢(输入卡顿?切 Tab 卡?滚动掉帧?),不要笼统地说「优化性能」;② 用 React DevTools Profiler 录制那次交互,看 Flamegraph(耗时占比)、Ranked(耗时排名)与「Why did this render?」(渲染原因:Parent re-rendered / Context changed / State changed);③ 用浏览器 Performance 面板看 React 之外的部分——是否有长任务(> 50ms)、布局抖动、大量 DOM 节点、或者其实是网络慢。确定瓶颈之后再动手,优化后重新测量。

  2. 「Profiler 里最有用的是哪个信息?」
    「Why did this render?」。它不仅告诉你「哪个组件渲染了」,还告诉你原因——常见的是「Parent re-rendered」(父组件重渲染带动)、「Context changed」、「Props changed(哪些 props)」、「State changed」。这些信息直接指向优化手段:父组件重渲染且 props 没变 → 用 memo/children;Context 变化 → 拆分 Context;State 变化 → 检查是否必要。

  3. 「怎么快速看出『渲染范围过大』?」
    打开 DevTools 的「Highlight updates when components render」,然后在页面上输入一个字或点一下——哪个区域闪就是哪些组件在渲染。如果「只有输入框闪」说明范围很小;如果「整页都在闪」说明状态放得太高,需要把状态下移到真正使用它的组件里。这是最直观、最快的诊断方式。

  4. 「渲染次数多一定是问题吗?」
    不一定。渲染 ≠ DOM 更新——「渲染」只是组件函数被调用(render 阶段),之后 React 会 diff,没有差异就不会有任何 DOM 操作。所以:① 一个「渲染 10 次、每次 0.1ms」的组件完全不需要优化;② 真正值得优化的是「渲染耗时长」或「渲染极其频繁」(如每秒几十次的动画组件)的组件。判断标准应该是 Profiler 里的耗时数据,而不是渲染次数本身。

  5. 「为什么开发环境下测出的性能不准?」
    因为开发构建包含额外的检查(StrictMode 双调用、PropTypes、更详细的警告、未压缩的代码、source map 等),这些都会显著增加耗时与渲染次数。所以:① 性能数字要看 production 构建;② 但开发环境也有价值——「双调用」能暴露「不纯的 render」与「缺失清理的副作用」,这些是正确性问题,会在生产环境变成真正的 bug(如重复请求)。

  6. 「除了 Profiler,还有什么常用的诊断手段?」
    四种:① console.log / 渲染计数(快速验证「某个组件是不是在频繁渲染」,但不要留在代码里);② performance.mark/measure(给关键路径打点,量化耗时);③ 自定义封装——写一个 useRenderProfiler 在开发环境对「超过 5ms 的渲染」发出警告;④ React.Profiler 组件(内置 API,可以编程式地收集每次渲染的耗时上报)。此外还有 Web Vitals(LCP/INP/CLS)用于线上度量。

  7. 「如果 Profiler 显示组件耗时都很小,但页面还是卡,怎么办?」
    说明瓶颈不在 React 的渲染。转向浏览器 Performance 面板查四类问题:① 长任务——某个业务计算(如大数组处理、复杂正则)阻塞了主线程,解法是「拆分(时间切片)/ Web Worker / 缓存」;② 布局抖动——在循环里「读尺寸 → 改样式」,解法是「批量读、批量写」;③ DOM 节点过多——布局/绘制的固定开销高,解法是「虚拟滚动 / 减少嵌套」;④ 网络或资源问题——首屏等待、资源阻塞,解法看第 6 章。

  8. 「线上性能问题怎么发现(而不是靠用户投诉)?」
    需要线上监控:① Web Vitals(LCP、INP、CLS、TTFB)——用 web-vitals 库采集;② 长任务监控——用 PerformanceObserver 监听 longtask;③ React 渲染耗时——用 <Profiler onRender> 上报超过阈值的渲染;④ 错误与资源监控——Sentry 之类。关键是要有「按页面/版本/设备维度的聚合视图」,才能判断「是否真的变差了」。这一点详见 8.6。

一句话速记

优化第一步是测量:先用 React DevTools Profiler 录制「具体那次慢交互」,看 Flamegraph(耗时占比)/ Ranked(耗时排名)/「Why did this render?」(渲染原因:Parent re-rendered / Context changed / State changed),用「Highlight updates」快速看渲染范围;再用浏览器 Performance 面板看「长任务(> 50ms)、布局抖动、DOM 数量、网络」这些非 React 的瓶颈;判断标准是「耗时」而不是「渲染次数」(渲染 ≠ DOM 更新);优化后必须重新测量;线上靠 Web Vitals + longtask 监控 + <Profiler onRender> 度量。

赞(0) 打赏
未经允许不得转载:陌上寒 » 5.1 定位性能问题

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫