
一句话:优化前必须先测量——用 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 本身会拖慢性能」(测量时可以先关掉高亮等功能)
八、面试延伸
- 「你怎么定位 React 的性能问题?」
分三步:① 明确症状与目标交互——是哪一次操作慢(输入卡顿?切 Tab 卡?滚动掉帧?),不要笼统地说「优化性能」;② 用 React DevTools Profiler 录制那次交互,看 Flamegraph(耗时占比)、Ranked(耗时排名)与「Why did this render?」(渲染原因:Parent re-rendered / Context changed / State changed);③ 用浏览器 Performance 面板看 React 之外的部分——是否有长任务(> 50ms)、布局抖动、大量 DOM 节点、或者其实是网络慢。确定瓶颈之后再动手,优化后重新测量。 -
「Profiler 里最有用的是哪个信息?」
「Why did this render?」。它不仅告诉你「哪个组件渲染了」,还告诉你原因——常见的是「Parent re-rendered」(父组件重渲染带动)、「Context changed」、「Props changed(哪些 props)」、「State changed」。这些信息直接指向优化手段:父组件重渲染且 props 没变 → 用memo/children;Context 变化 → 拆分 Context;State 变化 → 检查是否必要。 -
「怎么快速看出『渲染范围过大』?」
打开 DevTools 的「Highlight updates when components render」,然后在页面上输入一个字或点一下——哪个区域闪就是哪些组件在渲染。如果「只有输入框闪」说明范围很小;如果「整页都在闪」说明状态放得太高,需要把状态下移到真正使用它的组件里。这是最直观、最快的诊断方式。 -
「渲染次数多一定是问题吗?」
不一定。渲染 ≠ DOM 更新——「渲染」只是组件函数被调用(render 阶段),之后 React 会 diff,没有差异就不会有任何 DOM 操作。所以:① 一个「渲染 10 次、每次 0.1ms」的组件完全不需要优化;② 真正值得优化的是「渲染耗时长」或「渲染极其频繁」(如每秒几十次的动画组件)的组件。判断标准应该是 Profiler 里的耗时数据,而不是渲染次数本身。 -
「为什么开发环境下测出的性能不准?」
因为开发构建包含额外的检查(StrictMode 双调用、PropTypes、更详细的警告、未压缩的代码、source map 等),这些都会显著增加耗时与渲染次数。所以:① 性能数字要看 production 构建;② 但开发环境也有价值——「双调用」能暴露「不纯的 render」与「缺失清理的副作用」,这些是正确性问题,会在生产环境变成真正的 bug(如重复请求)。 -
「除了 Profiler,还有什么常用的诊断手段?」
四种:①console.log/ 渲染计数(快速验证「某个组件是不是在频繁渲染」,但不要留在代码里);②performance.mark/measure(给关键路径打点,量化耗时);③ 自定义封装——写一个useRenderProfiler在开发环境对「超过 5ms 的渲染」发出警告;④React.Profiler组件(内置 API,可以编程式地收集每次渲染的耗时上报)。此外还有 Web Vitals(LCP/INP/CLS)用于线上度量。 -
「如果 Profiler 显示组件耗时都很小,但页面还是卡,怎么办?」
说明瓶颈不在 React 的渲染。转向浏览器 Performance 面板查四类问题:① 长任务——某个业务计算(如大数组处理、复杂正则)阻塞了主线程,解法是「拆分(时间切片)/ Web Worker / 缓存」;② 布局抖动——在循环里「读尺寸 → 改样式」,解法是「批量读、批量写」;③ DOM 节点过多——布局/绘制的固定开销高,解法是「虚拟滚动 / 减少嵌套」;④ 网络或资源问题——首屏等待、资源阻塞,解法看第 6 章。 -
「线上性能问题怎么发现(而不是靠用户投诉)?」
需要线上监控:① 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>度量。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!