
一句话:Profiler 的价值是「把『这次交互渲染了哪些组件、各花了多久、为什么渲染』量出来」——它有三个视图(火焰图 / 排名 / 时间线)与两个编程式 API(
<Profiler onRender>),关键概念是「actualDurationvsbaseDuration」,后者能直接告诉你「记忆化到底省了多少」。
一、四个视图:各自回答什么问题
⭐⭐ DevTools 的 Profiler 面板有四个视图,**不要只盯着火焰图**:
① 【Flamegraph(火焰图)】——回答「**这一次 commit 里,时间花在哪棵子树上**」
· 横轴:组件树的嵌套关系(左侧是父、右侧是子)
· 宽度:该组件「渲染耗时」占本次 commit 的比例
· 颜色:灰=本次未渲染;蓝→黄→橙:耗时递增
② 【Ranked(排名)】——回答「**哪些组件最贵**」
· 按「自渲染耗时」从高到低排序(不是按树结构)
· 可以切换为「按渲染次数排序」
· ⭐ 优化从这里看最直接:排第一的就是优先优化对象
③ 【Timeline(时间线)】——回答「**哪一次交互触发了哪些渲染**」
· 横轴是时间,每一次 commit 是一个色块
· 点某个色块 → 看到那次 commit 的火焰图
· ⭐ 用来定位「某个操作为什么触发了多次渲染」
④ 【Interactions(交互)】——回答「**这次交互一共花了多久**」(实验性)
· 把「一次点击导致的多个 commit」聚合成一个「交互」
· 适合看「用户操作到界面稳定的总时长」
⭐ 火焰图里的三种「条形」要分清:
① 【实心条 + 较宽】
→ 这个组件「渲染了,且耗时显著」(优化重点)
② 【灰条(没有填充)】
→ 这个组件「本次没有渲染」(被 memo 跳过了,或者不在更新路径上)
→ ⭐ 一片灰是好事(说明传播被阻断了)
③ 【细边框 + 内部空白("未渲染但遍历过")】
→ 这个组件「被 React 访问了,但因为它自己的 state/props 没变而跳过」
→ 与「完全没访问」不同(后者连遍历都不会)
⚠️ 这个区别很细,但对「理解 memo 的实际效果」有用
⭐⭐⭐ 「Why did this render?」——Profiler 最有价值的功能
选中一个组件,右侧会显示它「为什么渲染」,常见四类原因:
① 【Props changed】——并列出「具体哪个 prop 变了」
→ 如果那个 prop 是「内联函数/对象」,就是「引用不稳定」的典型症状(见 5.8)
② 【State changed】——自己的 state 变了(合理)
③ 【Parent re-rendered】——⭐ 父组件重渲染带上来的(**最常见的优化点**)
→ 说明需要 memo / children 透传 / 状态下移(见 5.4)
④ 【Context changed】——订阅的 Context 变了(见 5.9)
⑤ 【Hooks changed】——自定义 Hook 的返回值变了
⭐ 用法:录制 → 选中一个「不该渲染的组件」→ 看它为什么渲染 → 对症优化
二、录制技巧:怎么录才有意义
⭐⭐ 五条「录制纪律」(不遵守的话,测出来的全是噪音):
① 【只录目标交互】
❌ 录一段「随便点点」的长片段(几百个 commit,无法分析)
✅ 明确目标:「输入 10 个字符」「打开 Modal」「切换 Tab」,然后精确录那一下
② 【多轮取中位数】
⚠️ 同一次操作,单次测量波动可能 ±30%(GC、JIT、后台任务)
✅ 同一交互重复 3~5 次,看「耗时分布」而不是「单次数值」
③ 【注意 dev 构建的失真】
⚠️ 开发构建有 StrictMode 双调用、更详细的警告、未压缩代码、
`useDebugValue` 记录等 → 渲染次数与耗时都显著高于 production
✅ 「正确性」在 dev 看(双调用暴露不纯/缺清理),
「性能数字」在 production build 看
④ 【忽略「极短渲染」】
⚠️ 0.05ms 的渲染在火焰图里也会显示,但优化它没有意义
✅ 关注「单个组件 > 1ms」或「某组件占本次 commit > 20%」
⑤ 【固定其他变量】
· 关掉 DevTools 的 "Highlight updates"(它本身有开销)
· 保持数据量一致(1000 条 vs 100 条,结论完全不同)
· 关掉其它标签页的重负载(无关的 CPU 竞争会污染结果)
// ✅ 「只录目标交互」的操作顺序(推荐)
// 1) 打开 Profiler → 点「录制」
// 2) 立刻做那一次交互(如输入一个字符)
// 3) 立刻停止录制(不要多做动作)
// 4) 在 Ranked 视图里找最贵的组件
// 5) 选中它 → 看 "Why did this render?"
三、<Profiler onRender>:编程式采集
import { Profiler } from 'react';
<Profiler
id="ProductList"
onRender={(id, phase, actualDuration, baseDuration, startTime, commitTime) => {
// id → "ProductList"
// phase → 'mount' | 'update' | 'nested-update'
// actualDuration → 本次实际渲染耗时(含子树)
// baseDuration → ⭐「没有记忆化时,完全重渲染整棵子树」的估计耗时
// startTime → 本次渲染开始
// commitTime → 本次提交时间
}}
>
<ProductList />
</Profiler>
⭐⭐⭐ `actualDuration` 与 `baseDuration` 的区别(这是本篇最重要的知识点):
· **`actualDuration`** = 本次渲染「实际」花了多久
(含子树,但**被 memo/useMemo 跳过的部分不计入**)
· **`baseDuration`** = 「**假设整棵子树每个组件都重新渲染**」的估计耗时
(由「子树里每个组件最近一次渲染的耗时」累加得出,**不含记忆化收益**)
⭐ 由此得出一个非常有用的判据:
记忆化收益 ≈ baseDuration / actualDuration
· 比值接近 1 → 记忆化几乎没起作用(要么没有 memo,要么依赖不稳定)
· 比值很大(如 10)→ 记忆化省掉了大量工作(说明结构 + memo 生效了)
⚠️ `baseDuration` 是「估计值」(它不重新渲染,只是累加缓存的历史耗时),
所以不要把它当成「真实的重渲染耗时」。
⭐ React 18 起 `onRender` 只有 6 个参数——
React 17 曾多一个 `interactions` 参数(用于关联「这次渲染由哪个交互触发」),
后来被移除了。
// ✅ 三个「最小侵入」的使用姿势
// ① 包在「整个页面」外面:看「每次交互的总渲染成本」
<Profiler id="Page" onRender={reportRender}>
<App />
</Profiler>
// ② 包在「可疑组件」外面:看它自己+子树的耗时
<Profiler id="DataTable" onRender={reportRender}>
<DataTable rows={rows} />
</Profiler>
// ③ 嵌套多个 Profiler:得到「分层的耗时归因」
<Profiler id="Page" onRender={reportRender}>
<Header />
<Profiler id="Sidebar" onRender={reportRender}><Sidebar /></Profiler>
<Profiler id="Main" onRender={reportRender}><Main /></Profiler>
</Profiler>
// ⭐ 嵌套后能算出「Main 占 Page 的比例」
// ⚠️ 每个 Profiler 都有少量开销,别包太多(生产环境尤其)
四、完整实现:一个 220 行的「渲染性能采集器」
// perf-probe.tsx —— 用 <Profiler> 做「慢渲染采集 + 分组件聚合 + 上报」
import React, { Profiler, useRef, useCallback, useContext, createContext } from 'react';
/** ============ 1. 采集上下文(统一上报出口) ============ */
export interface RenderSample {
id: string;
phase: 'mount' | 'update' | 'nested-update';
actualDuration: number;
baseDuration: number;
commitTime: number;
/** 记忆化收益比(baseDuration / actualDuration) */
memoRatio: number;
/** 本次 commit 里同批次上报的其它 Profiler(用于还原「这一次 commit 全貌」) */
commitId: number;
}
interface PerfOptions {
/** 超过这个耗时才上报(默认 8ms) */
thresholdMs?: number;
/** 上报函数(默认在开发环境打 console) */
onSample?: (sample: RenderSample) => void;
/** 是否总是上报 mount(默认 true) */
reportMounts?: boolean;
}
const PerfContext = createContext<PerfOptions>({});
export function PerfProvider({ children, ...options }: PerfOptions & { children: React.ReactNode }) {
return <PerfContext.Provider value={options}>{children}</PerfContext.Provider>;
}
/** ============ 2. 组件级探针:<PerfProbe name="X"> ============ */
export function PerfProbe({ name, children }: { name: string; children: React.ReactNode }) {
const options = useContext(PerfContext);
const commitIdRef = useRef(0);
const handleRender = useCallback(
(
id: string,
phase: 'mount' | 'update' | 'nested-update',
actualDuration: number,
baseDuration: number,
_startTime: number,
commitTime: number
) => {
const threshold = options.thresholdMs ?? 8;
const shouldReport =
actualDuration >= threshold || (options.reportMounts !== false && phase === 'mount');
if (!shouldReport) return;
const sample: RenderSample = {
id,
phase,
actualDuration: Number(actualDuration.toFixed(3)),
baseDuration: Number(baseDuration.toFixed(3)),
commitTime: Number(commitTime.toFixed(1)),
// ⭐ 记忆化收益:> 1 说明「如果不做记忆化会更慢」
memoRatio: actualDuration > 0 ? Number((baseDuration / actualDuration).toFixed(2)) : 1,
commitId: commitIdRef.current++,
};
if (options.onSample) options.onSample(sample);
else if (process.env.NODE_ENV !== 'production') {
console.warn(
`[perf] ${sample.id} ${sample.phase}:` +
`实际 ${sample.actualDuration}ms / 未记忆化估计 ${sample.baseDuration}ms ` +
`(收益 ${sample.memoRatio}×)`
);
}
},
[options]
);
return (
<Profiler id={name} onRender={handleRender}>
{children}
</Profiler>
);
}
/** ============ 3. 聚合分析:把「慢渲染」聚合成「优化清单」 ============ */
export interface AggregateItem {
id: string;
count: number; // 渲染次数
totalMs: number; // 累计耗时
maxMs: number; // 单次最大耗时
avgMs: number;
avgMemoRatio: number; // 平均记忆化收益
/** 优化建议(基于 memoRatio 自动生成) */
advice: string;
}
export function aggregateSamples(samples: RenderSample[]): AggregateItem[] {
const map = new Map<string, RenderSample[]>();
for (const s of samples) {
const arr = map.get(s.id) ?? [];
arr.push(s);
map.set(s.id, arr);
}
const items: AggregateItem[] = [];
map.forEach((list, id) => {
const totalMs = list.reduce((sum, s) => sum + s.actualDuration, 0);
const maxMs = Math.max(...list.map((s) => s.actualDuration));
const avgMemoRatio = list.reduce((sum, s) => sum + s.memoRatio, 0) / list.length;
// ⭐ 自动给建议:这是「采集 → 行动」的桥
let advice = '正常';
if (avgMemoRatio < 1.2 && list.length > 20) {
advice = '渲染频繁且「记忆化收益低」→ 检查 props 引用稳定性 / 是否需要 memo';
} else if (maxMs > 16) {
advice = '存在超过一帧预算的渲染 → 拆分组件 / useMemo 缓存昂贵计算 / 虚拟滚动';
} else if (list.length > 100) {
advice = '渲染次数过多 → 检查是否状态下移(状态放太高会引起大范围重渲染)';
} else if (avgMemoRatio > 5) {
advice = '记忆化收益很高 → 说明当前结构是对的,保持';
}
items.push({
id,
count: list.length,
totalMs: Number(totalMs.toFixed(2)),
maxMs: Number(maxMs.toFixed(3)),
avgMs: Number((totalMs / list.length).toFixed(3)),
avgMemoRatio: Number(avgMemoRatio.toFixed(2)),
advice,
});
});
// ⭐ 按「累计耗时」排序(优化优先级 = 累计影响,而不是单次最大)
return items.sort((a, b) => b.totalMs - a.totalMs);
}
/** ============ 4. 使用示例:把探针接进一个真实页面 ============ */
import { createReporter } from './rum'; // 见 5.11
const reportRum = createReporter('/api/rum', __APP_VERSION__);
const samples: RenderSample[] = [];
export function App() {
const handleSample = (sample: RenderSample) => {
samples.push(sample);
// ⭐ 只上报「明显异常」的样本(避免把线上监控打爆)
if (sample.actualDuration > 50) {
reportRum({
name: 'LCP', // 复用 RUM 的上报通道(示例)
value: sample.actualDuration,
target: `${sample.id}:${sample.phase}`,
breakdown: { baseDuration: sample.baseDuration, memoRatio: sample.memoRatio },
url: location.pathname,
release: __APP_VERSION__,
device: { connection: 'unknown', memory: null, cores: null },
firstVisit: false,
ts: Date.now(),
});
}
// 开发期:把聚合结果打出来(每 20 个样本刷新一次)
if (process.env.NODE_ENV !== 'production' && samples.length % 20 === 0) {
console.table(aggregateSamples(samples).slice(0, 8));
}
};
return (
<PerfProvider thresholdMs={8} onSample={handleSample}>
<PerfProbe name="App">
<Page />
</PerfProbe>
</PerfProvider>
);
}
function Page() {
return (
<>
<PerfProbe name="Header"><Header /></PerfProbe>
<PerfProbe name="ProductList"><ProductList /></PerfProbe>
<PerfProbe name="Recommendations"><Recommendations /></PerfProbe>
</>
);
}
⭐⭐ 为什么「累计耗时」比「单次最大」更适合排优先级?
优化是「投入产出比」的选择:
· 某组件单次 30ms,但只渲染 1 次(总影响 30ms)→ 收益有限
· 某组件单次 3ms,但每次输入都渲染(100 次 → 总影响 300ms)→ ⭐ 更值得优化
→ 所以「按 totalMs 排序」通常比「按 maxMs 排序」更能指导优化。
(当然,「单次超长」如果发生在关键路径上,也要单独处理,比如 > 50ms 会阻塞输入。)
⭐⭐ 三个「自动建议」的规则(把数据变成行动):
① 「渲染次数多 + 记忆化收益低」→ 检查 props 引用稳定性(内联函数/对象)或是否该 memo
② 「单次耗时 > 一帧预算(16ms)」→ 拆分组件 / 缓存昂贵计算 / 虚拟滚动
③ 「渲染次数极多(>100 次)」→ 状态下移(状态放太高导致大范围重渲染)
五、与 Chrome Performance 面板的分工
| 维度 | React DevTools Profiler | Chrome Performance 面板 |
|---|---|---|
| 视角 | 组件级(哪个组件慢) | 浏览器级(JS/样式/布局/绘制) |
| 能看到 | 组件渲染耗时、渲染原因、记忆化收益 | 长任务、Layout 抖动、绘制、GC、FPS |
| 看不到 | DOM/样式/布局的具体成本 | 「哪个 React 组件」在耗时 |
| 适合 | 定位「哪个组件该优化」 | 定位「慢在哪一类工作」 |
| 时间精度 | 组件渲染耗时(含子树) | 调用栈级(可以细到函数) |
⭐⭐ 正确的配合顺序:
① 先用 **Profiler** 找到「哪个组件的渲染耗时占比高」
② 如果那个组件的耗时「看起来不该那么贵」(代码很简单),
就用 **Performance 面板**看「它渲染时到底在干什么」:
· 是「Layout 抖动」?(读尺寸 + 改样式交替)
· 是「样式重算」?(大量 DOM 节点变化)
· 是「Paint 太重」?(阴影/滤镜/大图)
③ 如果两次都指向「框架代码占比高」→ 检查「渲染次数是否过多」(用 Profiler 的 Ranked 按次数排序)
⭐ 一句话:
Profiler 回答「**哪个组件慢**」,Performance 回答「**为什么慢**」。
六、本篇特有的坑
// ① 在开发构建里测「性能数字」(StrictMode 双调用 + 未压缩代码 → 数字偏高)
// ✅ 性能看 production build;正确性(不纯渲染/缺清理)看 development
// ② 只录「一大段随便点的操作」
// ✅ 精确录「一次目标交互」,重复多次取中位数
// ③ 只看「火焰图」不看「Ranked」
// ⚠️ 火焰图受嵌套影响(看起来「父组件很宽」可能只是因为包含子树)
// ✅ Ranked 的「自耗时排序」更直接
// ④ 把「灰条」当成「有性能问题」
// ⚠️ 灰色 = 本次未渲染 = 好事(说明传播被阻断了)
// ⑤ 优化「0.05ms 的渲染」
// ✅ 关注 > 1ms 或占本次 commit > 20% 的组件
// ⑥ 忽略 `baseDuration`(不知道记忆化到底有没有用)
// ✅ 用 baseDuration / actualDuration 判断「记忆化收益」
// ⑦ 把 `baseDuration` 当成「真实的重渲染耗时」
// ⚠️ 它是「累加历史耗时」的估计值,不是「重新渲染一遍」的真实耗时
// ⑧ 在生产环境「包一大堆 `<Profiler>`」
// ⚠️ 每个 Profiler 都有开销(且 Profiler 在生产默认仍会计时)
// ✅ 只包关键路径,或通过环境变量按需启用
// ⑨ 用 Profiler 测「非 React 的性能问题」
// ⚠️ 它看不到「布局抖动、DOM 数量、网络」→ 用 Performance 面板
// ⑩ 认为「渲染次数多 = 有性能问题」
// ⚠️ 渲染 ≠ DOM 更新(见 4.6);要看「耗时」
// 「渲染 10 次 × 0.1ms」完全不值得优化
// ⑪ 忽略「Why did this render?」里的「Props changed」细节
// ⚠️ 它会指出「具体哪个 prop 变了」→ 这是找「引用不稳定」的最快路径
// ⑫ 用「一次录制」的结论做架构决策
// ⚠️ 单次测量波动可达 ±30% → 必须多次取样
// ⑬ 在「输入框快速输入」时录制(会录到几十个 commit,无法分析)
// ✅ 录「输入一个字符」这一下
// ⑭ 忘记「DevTools 自身会拖慢页面」
// ✅ 测量时关掉「Highlight updates」等重功能
// ⑮ 把 Profiler 的结论「直接当优化清单」而不验证
// ✅ 优化后必须「重新录制」确认改善(见 5.1 的决策流程)
// ⑧ 的完整处理:只在需要时启用 Profiler
const ENABLE_PERF_PROBE = import.meta.env.VITE_ENABLE_PERF === '1';
export function PerfProbe({ name, children }: { name: string; children: React.ReactNode }) {
if (!ENABLE_PERF_PROBE) return <>{children}</>; // ⭐ 生产默认关闭
return <Profiler id={name} onRender={handleRender}>{children}</Profiler>;
}
七、面试延伸
- 「你怎么用 Profiler 定位性能问题?」
分四步:① 只录目标交互(如「输入一个字符」),并重复 3~5 次取中位数(单次波动可达 ±30%);② 在 Ranked 视图里按「耗时」或「渲染次数」排序,找出累计影响最大的组件;③ 选中它看「Why did this render?」——如果是 Parent re-rendered,说明需要memo/children透传/状态下移(见 5.4);如果是 Props changed,看是具体哪个 prop(通常是内联函数/对象);④ 优化后重新录制对比。注意「性能数字看 production build」,开发构建有 StrictMode 双调用与额外检查,数字会偏高。 -
「火焰图和 Ranked 视图有什么区别?」
① 火焰图按「组件树结构」组织(横轴是嵌套关系),宽度表示耗时占本次 commit 的比例——它的优点是「能看出时间花在哪棵子树上」,缺点是「父组件的宽度包含了子树,容易误判」(看起来父组件很宽,其实它自己很便宜);② Ranked 按「自渲染耗时或渲染次数」排序,直接给出「谁最贵」的清单,是优化优先级的直接依据。实践上「先用 Ranked 找目标,再用火焰图看它在树里的位置」。 -
「
actualDuration和baseDuration有什么区别?」
actualDuration是「本次渲染实际耗时」(被 memo 跳过的部分不计入);baseDuration是「假设整棵子树每个组件都重新渲染」的估计耗时(由子树里每个组件「最近一次渲染的耗时」累加得出)。价值在于比值:baseDuration / actualDuration≈ 记忆化收益——接近 1 说明「记忆化几乎没起作用」(没有 memo 或依赖不稳定),比值大说明「省掉了大量工作」。注意baseDuration是估计值(不重新渲染),不能当「真实重渲染耗时」。 -
「Profiler 的『Why did this render?』怎么用?」
它把渲染原因分成五类:Props changed(并列出具体哪个 prop)、State changed、Parent re-rendered(最常见)、Context changed、Hooks changed。用法是「录制 → 选中一个不该渲染的组件 → 看原因 → 对症优化」:① Parent re-rendered → 用memo+ 稳定 props,或用children透传 / 状态下移(结构性手段优先);② Props changed 且是函数/对象 → 引用不稳定(useCallback/useMemo/提到模块外);③ Context changed → 拆分 Context。 -
「Profiler 看不到什么?」
它只看 React 的渲染耗时,看不到:① DOM/样式/布局/绘制的成本(如「布局抖动」「大量节点导致样式重算」——要用 Chrome Performance 面板);② 网络与资源加载(用 Network 面板 / RUM,见 5.11);③ 非 React 的 JS(第三方库的同步计算、长任务);④ 内存与 GC(用 Memory 面板,见 5.13)。所以正确顺序是:Profiler 找「哪个组件慢」→ Performance 面板看「为什么慢」→ 如果两次都指向框架,就查「渲染次数是否过多」。 -
「怎么用
<Profiler>做线上监控?」
三步:① 用<Profiler onRender>包裹关键路径组件(不要包太多,每个都有开销,建议用环境变量控制);② 在onRender里只在超过阈值时上报(如actualDuration > 50,或phase === 'mount'),并带上「组件 id / phase / actualDuration / baseDuration / 路由 / 版本」;③ 上报后按组件聚合(累计耗时、次数、平均记忆化收益),并按累计耗时排序得到优化清单。注意:线上数据要「分级采样」(差的全报、好的采样),避免把监控打爆。 -
「为什么『按累计耗时排序』比『按单次最大耗时排序』更好?」
因为优化是「投入产出比」的选择:① 某组件单次 30ms 但只渲染 1 次 → 总影响 30ms,优化收益有限;② 某组件单次 3ms 但每次输入都渲染 100 次 → 总影响 300ms,更值得优化。所以「按 totalMs 排序」通常更能指导优化。例外是「单次超长且出现在关键路径上」(如 > 50ms 会阻塞输入、影响 INP)——这类问题即使只发生一次也要单独处理。 -
「开发环境和生产环境测出来的数字为什么差别大?」
因为开发构建包含大量额外工作:① StrictMode 双调用(组件函数执行两次、Effect 也执行两次);② 更详细的警告与检查(如useEffect依赖校验、不可变检查);③ 未压缩的代码(体积大、执行慢);④useDebugValue的记录;⑤ source map;⑥ DevTools 的 hooks 追踪。所以「性能数字必须在 production build 里测」;而「正确性(不纯的渲染、缺失的清理)要在 development 里测」(因为只有开发环境会双调用暴露这些问题)。
一句话速记
Profiler 的四个视图各有分工:Flamegraph 看「时间花在哪棵子树」、Ranked 看「谁最贵」(优化优先级的直接依据)、Timeline 看「哪次交互触发了哪些 commit」、Interactions 看「一次交互的总时长」;灰条 = 本次未渲染 = 好事;「Why did this render?」把原因分为 Props changed / State changed / Parent re-rendered / Context changed / Hooks changed(Parent re-rendered 最常见 → 用
memo/children/状态下移);actualDuration是实际耗时(跳过的不计入),baseDuration是「不做记忆化时的估计」——两者的比值就是「记忆化收益」,接近 1 说明 memo 没起作用;录制要「**只录目标交互 + 多次取中位数 + 忽略 <1ms + 关掉 DevTools 重功能**」;**性能数字看 production build、正确性看 development build**;**Profiler 回答「哪个组件慢」,Chrome Performance 回答「为什么慢」**;线上采集要「**只包关键路径 + 超标才上报 + 按累计耗时排序**」(累计影响 > 单次峰值,除非峰值出现在关键路径)。



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