性能优化不是让 Profiler 图变得更短,而是在明确用户场景、测量环境和业务约束下,用可重复证据证明关键体验改善,并确保收益在后续版本中持续存在。
一、为什么“感觉更快”不是性能结论
React 项目中常见的性能工作方式是:看到页面卡顿,给组件加 memo;看到 Bundle 较大,拆几个 Chunk;本地点击顺畅后就宣布优化完成。这种流程无法回答:
- 慢的是首次加载、点击响应、React Render,还是浏览器 Layout;
- 问题影响所有用户,还是只影响低端设备、弱网或大数据账号;
- 修改改善了用户指标,还是只减少了理论 Render 次数;
- 首屏更快是否以更多流量、内存或后续交互变慢为代价;
- 一次本地录制是否只是缓存、JIT、GC 或后台任务造成的偶然结果;
- 下个版本是否会悄悄把收益全部回退。
性能工程必须形成闭环:
图中 Field Data 指真实用户监控(Real User Monitoring,RUM),Lab Data 指受控环境中的可重复测试。Field 告诉团队“谁在何时变慢”,Lab 帮助解释“为什么慢”;React Profiler 与浏览器 Performance Trace 再把时间归因到不同执行阶段。
本文覆盖性能方法,不提前展开下一篇 Web Vitals 的各指标阈值,也不替后续“React 渲染性能”“列表与大数据”“资源与网络”选择具体优化手段。React、浏览器和 DevTools 会持续演进,Profiler 字段与面板能力应以项目锁定版本和目标浏览器为准。
核心结论
- 先定义用户场景和成功标准,再选择指标与工具;工具输出本身不是目标。
- Performance Budget 必须绑定 Route、Journey、设备、网络、缓存和分位数,不能只有一个全站平均值。
- User-centric Metrics 描述用户等待和交互结果,Bundle Size、Render Count 等只是不完整的诊断代理。
- Field Data 提供真实分布,Lab Data 提供受控复现;两者用途不同,不能互相替代。
- React Profiler 主要解释组件 Render 与 Commit 关系,不能测量完整网络、Style、Layout、Paint 和像素显示时间。
- Browser Performance Panel 用于观察主线程、网络、脚本、Layout、Paint 和交互时序,但不直接告诉你哪个 React State 设计错误。
- CPU 与 Network Throttling 是可重复压力条件,不是某台真实设备或网络的精确模拟。
- P50 表示典型体验,P95/P99 表示长尾;样本量、分群和采样方式决定分位数是否可信。
- 性能优化必须通过单变量实验验证因果关系,并同时观察功能、错误率、内存和流量等护栏。
- 性能回归应组合确定性预算、实验室趋势和线上 RUM,避免把高噪声单次计时直接设为硬门禁。
- 开发模式、Strict Mode、扩展和日志会改变测量结果,最终结论应来自生产构建或专用 Profiling Build。
- 任何优化都应保留基线、环境、Trace、修改和复测证据,确保结果可以被他人复现。
二、先把用户问题转换为可测场景
“订单页很卡”无法直接测量。应把它改写为具备起点、终点和条件的 User Journey:
场景:已登录用户在订单列表输入筛选词
起点:输入事件被浏览器接收
终点:与当前筛选词一致的列表完成可见更新
条件:中端移动设备、生产构建、5000 条本地记录、已加载页面
护栏:结果正确,无输入丢失,内存和错误率不恶化2.1 不同用户问题需要不同指标
| 用户问题 | Journey 起点 | Journey 终点 | 主要证据 |
|---|---|---|---|
| 首次打开慢 | Navigation Start | 主要内容可见且可用 | RUM、Navigation/Resource Timing、Trace |
| 点击后迟迟无反馈 | Input Event | 下一次视觉反馈 | Interaction Metric、Event Timing、Trace |
| 输入筛选卡顿 | Key Input | 对应结果可见 | User Timing、Profiler、Trace |
| 列表滚动掉帧 | Scroll/Input | 连续帧完成 | Frame/Long Task、Trace |
| 页面切换慢 | Link Activation | 新 Route 内容可用 | Route Trace、Network、Code/Data Timing |
| 保存耗时长 | Submit | 服务端确认并展示结果 | Business Timing、Network、Mutation Trace |
同一个“慢”可能同时包含多个阶段:
只测 handler() 执行时间会漏掉排队、React、Layout 和 Paint;只看 React Render Duration 也无法证明用户已经看到结果。
2.2 先写 Measurement Contract
每个关键 Journey 建议有一份测量契约:
journey: orders-filter
start: user-input-received
end: matching-list-committed
build: production
data_set: 5000-orders-v3
segments:
- desktop-mid-warm
- mobile-mid-warm
metrics:
- interaction-duration
- react-actual-duration
- long-task-count
guardrails:
- result-correctness
- error-rate
- memory-growth这里的 matching-list-committed 仍只是 React/DOM 里程碑,不自动等于像素已经显示。指标名称必须说明测量终点,避免团队把不同语义的数字放在同一图表中比较。
三、Performance Budget:把性能变成工程约束
Performance Budget 是项目允许消耗的时间、字节、主线程工作或资源数量上限。它的价值不是生成一张分数表,而是在需求和代码合入前明确“为了这个功能,最多愿意支付多少性能成本”。
3.1 预算至少包含三类
| 预算类型 | 示例 | 作用与限制 |
|---|---|---|
| 用户结果预算 | 页面可用时间、交互响应分位数 | 最接近体验,但噪声和环境影响较大 |
| 执行预算 | 主线程阻塞、Long Task、React Render | 帮助归因,不能单独代表体验 |
| 资源预算 | Initial JS、CSS、图片、请求数 | 易于 CI 检查,但只是传输与执行成本代理 |
还应设置 Guardrail:
- Crash、JS Error 和请求失败率不得上升;
- 业务结果与可访问性不得退化;
- 内存、流量、电量和服务端 QPS 不得出现不可接受增长;
- 低端设备和关键地区不能只因总体平均值改善而变差。
3.2 预算必须绑定场景和分群
下面只是预算文件的格式示例,数字必须由项目基线、用户目标和业务价值确定,不能当作通用阈值:
route: orders
journey: filter-existing-list
profile: mobile-mid-production
budgets:
interaction_p95_ms: 240
react_render_p95_ms: 80
long_tasks_per_interaction_max: 1
route_js_gzip_kb_max: 180
guardrails:
result_error_rate_max: 0.001
heap_growth_mb_max: 8预算应回答:
- 冷缓存还是热缓存;
- 首次访问还是重复访问;
- 哪个 Route 和 Journey;
- 哪类设备、浏览器和网络;
- P50、P75、P95 还是其他统计量;
- 数据规模和账号复杂度;
- 测量窗口、版本和样本下限。
“全站加载小于 2 秒”通常不可执行,因为不同页面、缓存和用户环境完全不同。
3.3 预算不是永远不变的常量
新业务可能合理增加成本,浏览器和设备结构也会变化。调整预算时应记录:
- 原预算和实际分布;
- 新功能带来的用户价值;
- 为什么不能在原预算内实现;
- 哪些资源或指标被放宽;
- 后续偿还计划和负责人。
不要让每次超标都通过“临时提高阈值”解决,否则 Budget 只是文档装饰。
四、User-centric Metrics:测用户等待,而不是代码自我感觉
用户中心指标围绕真实任务:内容何时出现、输入何时得到反馈、界面是否稳定、操作何时完成。Web Vitals 是其中一组标准化指标,具体的 LCP、INP、CLS、TTFB 和 Attribution 会在下一篇展开。
4.1 业务 Journey 也需要指标
标准指标无法覆盖所有业务完成点。例如:
- 订单列表的第一屏何时包含真实数据,而不是 Skeleton;
- 编辑器输入后预览何时与当前文本一致;
- 点击支付后何时得到可确认的服务端状态;
- 从详情返回列表何时恢复筛选和滚动位置。
可以使用 User Timing API 标记应用里程碑:
function markOrderFilterStarted(query: string) {
performance.mark('orders-filter:start', {
detail: { queryLength: query.length },
});
}
function markOrderFilterCommitted(resultCount: number) {
performance.mark('orders-filter:commit', {
detail: { resultCount },
});
const measure = performance.measure(
'orders-filter:input-to-commit',
'orders-filter:start',
'orders-filter:commit',
);
enqueuePerformanceSample({
name: measure.name,
duration: measure.duration,
});
}performance.mark 创建高精度时间轴标记,performance.measure 计算两个标记之间的 Duration。上例只测到应用定义的 Commit 标记,不等于浏览器已经 Paint;要分析视觉完成时间,应结合 Event Timing、Frame、截图或 Performance Trace。
标记的 detail 也可能进入 Trace 和遥测。不要放入搜索原文、邮箱、订单号等高基数或敏感数据。
4.2 Field Data 与 Lab Data
| 维度 | Field / RUM | Lab |
|---|---|---|
| 用户与设备 | 真实且分布复杂 | 预先固定 |
| 网络与后端 | 真实波动 | 可控或模拟 |
| 可复现性 | 较低 | 较高 |
| 根因调试 | 上下文有限 | 可录制完整 Trace |
| 长尾发现 | 强 | 依赖选定场景 |
| 发布前验证 | 有滞后 | 强 |
合理流程是:
- RUM 发现特定 Route、版本、设备或网络的分位数异常;
- Lab 使用接近该分群的条件复现;
- Profiler/Trace 定位根因并做单变量实验;
- 发布后回到同一 RUM Segment 验证真实收益。
Lab 通过不代表线上所有人都快,Field 变慢也不一定能仅靠聚合数据定位代码行。
4.3 RUM 采集必须治理
- 对高频事件采样,避免每个 Commit 都上报;
- 使用 Route ID、Journey ID、版本、设备档位等低基数维度;
- 不记录完整 URL、用户输入和敏感业务字段;
- 记录页面可见性、Back/Forward Cache、缓存状态等必要上下文;
- 明确异常值、超时和页面中途关闭如何统计;
- 设定最小样本量,低流量页面不要强行展示不稳定 P99;
- 对 Bot、自动化流量和内部测试进行合理隔离。
五、React Profiler:定位组件树中的工作
React Profiler 有两种常见形式:
- React Developer Tools 中的交互式 Profiler;
<Profiler>API 的onRender回调。
5.1 <Profiler> 提供什么
import { Profiler, type ProfilerOnRenderCallback } from 'react';
const onRender: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
) => {
enqueueProfilerSample({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
});
};
function OrdersRoute() {
return (
<Profiler id="OrdersTable" onRender={onRender}>
<OrdersTable />
</Profiler>
);
}主要字段:
| 字段 | 含义 |
|---|---|
id |
当前 Profiler 边界名称 |
phase |
Mount、Update 或当前版本支持的其他阶段 |
actualDuration |
本次更新实际用于 Render 该子树的时间估计 |
baseDuration |
无优化地重新 Render 整个子树的基准成本估计 |
startTime |
本次 Render 开始时间 |
commitTime |
当前 Commit 时间,可用于合并同一次 Commit 的多个边界 |
actualDuration 与 baseDuration 可帮助判断 Memoization 是否跳过了大量工作,但它们不是网络时间,也不是 Layout、Paint 或用户可见完成时间。
onRender 运行在性能敏感路径上,不要同步发送网络请求、打印大量日志或执行复杂聚合。应采样并异步批量处理。
5.2 交互式 Profiler 应回答什么
- 哪次 Commit 对应用户感知到的慢交互;
- 哪些组件参与了该 Commit;
- 时间主要集中在哪个子树;
- 组件为何 Render:自身 State、Context、父级或 Props;
- 修改后 Commit 次数和 Duration 是否真正下降;
- 优化是否只把时间转移到另一个组件或 Commit。
看到组件 Render 并不代表它需要 memo。如果 Render 廉价、没有 DOM 变化,或用户时间主要花在 Layout,优化它不会产生可见收益。
5.3 Profiler 的边界
React 官方文档明确指出 Profiling 会增加额外开销,常规 Production Build 默认关闭详细 Profiling。需要接近生产执行方式时,应使用工具链提供的专用 Profiling Build,并在代表性设备复测。
Profiler 不能独立回答:
- 请求何时开始和结束;
- 事件在主线程队列中等了多久;
- DOM Mutation 后 Style/Layout/Paint 花了多久;
- 第三方脚本、JSON Parse、GC 或普通业务函数的完整成本;
- 像素何时真正显示在屏幕上。
这些问题需要浏览器 Performance Trace。
六、Browser Performance Panel:查看从事件到像素的主线程证据
浏览器 Performance 面板通常能同时展示:
- Input、Animation Frame 和截图;
- Network Request 与资源加载;
- JavaScript Task、Call Tree 和 Bottom-up;
- Style Recalculation、Layout、Paint、Composite;
- Long Task、GC 和部分内存活动;
- User Timing Mark/Measure;
- 支持版本中的 React Performance Track。
6.1 一次有效录制的步骤
- 使用生产构建或明确的 Profiling Build;
- 关闭无关标签页、扩展和开发日志;
- 固定数据集、缓存状态、CPU 与网络条件;
- 预热需要预热的代码路径;
- 开始录制后只执行目标交互;
- 停止录制,先定位用户事件和可见结果;
- 查看中间是否存在 Long Task、Script、Layout 或 Paint;
- 使用 Bottom-up/Call Tree 找主要 Self Time 与 Total Time;
- 保存 Trace,并记录 Build Commit、环境和操作脚本;
- 修改后在同样条件重复多轮。
录制时间过长会产生巨大 Trace,后台任务也更难排除。一次只录制一个清晰 Journey。
6.2 Top-down 与 Bottom-up
- Call Tree / Top-down:从调用入口理解执行链;
- Bottom-up:按函数聚合耗时,定位主要成本贡献者;
- Self Time:函数自身工作,不含子调用;
- Total Time:函数及其后代总成本。
只看到某个函数 Total Time 很高,不代表优化该函数自身就有效;成本可能来自它调用的 Layout Read、JSON Parse 或第三方库。
6.3 React Profiler 与 Performance Panel 的分工
| 问题 | 优先工具 |
|---|---|
| 哪个 React 子树 Render 较慢 | React Profiler |
| 为什么组件重新 Render | React DevTools Profiler |
| 事件排队和主线程 Long Task | Performance Panel |
| 网络、脚本解析与执行 | Network + Performance |
| Style、Layout、Paint | Performance Panel |
| 一次 Commit 包含哪些组件 | React Profiler |
| 用户事件到可见更新全链路 | Performance Trace + 用户指标 |
两个工具不是竞争关系。Profiler 缩小 React 范围,Performance Trace 判断 React 是否真的是主耗时阶段。
七、CPU Throttling:放大主线程瓶颈
CPU Throttling 可以让本地快速机器上的短任务变长,从而更容易观察低算力用户的风险。它适合:
- 暴露昂贵同步计算和巨大 Render;
- 比较同一机器上修改前后的相对变化;
- 验证 Long Task、输入响应和动画压力;
- 在 CI/Lab 中建立较稳定的统一条件。
7.1 CPU Throttling 不能模拟什么
一个倍率不能精确模拟目标手机,因为真实设备还受到以下因素影响:
- CPU 架构、核心数量和调度;
- 内存容量、带宽与 GC 行为;
- GPU、屏幕刷新率和浏览器实现;
- 温控、低电量模式和后台进程;
- 操作系统版本和厂商策略。
因此结论应分两层:Throttling 用于可重复比较,代表性真机用于最终验证。不要把“4x Slowdown 下耗时 400 ms”直接解释为某台手机一定耗时 400 ms。
7.2 控制预热与运行顺序
JavaScript JIT、Module Cache、图片解码、HTTP Cache 和 GC 都会影响结果。建议:
- 冷启动和热交互分开测试;
- 明确每轮是否重载页面、清除 Cache;
- 先运行预热轮,不把它混入热路径统计;
- A/B 交替运行,降低温度和后台波动的时间偏差;
- 每轮间检查页面状态和数据规模一致;
- 保存异常轮次原因,不只删除“难看数据”。
八、Network Throttling:区分延迟、带宽与应用等待
网络条件至少影响:
- DNS、连接和 TLS;
- Request Latency 与 Server TTFB;
- Response Transfer;
- Chunk、图片和字体的并发竞争;
- Retry、Timeout、Prefetch 和 Cache 命中;
- 数据到达后的 Parse、Render 和 Paint。
8.1 冷缓存、热缓存和 Service Worker 是不同场景
测试前应明确:
| 场景 | 目的 |
|---|---|
| Empty Cache + Hard Reload | 首次访问与资源成本 |
| HTTP Cache Warm | 重复访问 |
| App Query Cache Warm | 客户端资源复用 |
| Service Worker Controlled | 离线或应用缓存策略 |
| Back/Forward Cache | History 恢复体验 |
只勾选 DevTools 的 Disable Cache 不一定覆盖应用内 Query Cache、Service Worker 和 CDN 行为。需要在 Trace/Network 中检查资源的真实来源。
8.2 节流不是完整真实网络
DevTools Profile 通常主要控制 Latency 和 Throughput,不能完整模拟蜂窝网络的抖动、丢包、切网、无线电唤醒和区域路由。最终仍需真实网络、远端测试节点和 RUM 验证。
判断慢请求时应拆分:
用户等待 = 排队 + 连接 + TTFB + 下载 + 解析 + React + 浏览器渲染压缩 Response 只能减少下载,无法直接解决高 TTFB;增加 Prefetch 可能改善导航,却增加无效流量和服务端 QPS。
九、P50、P95、P99:用分布理解用户体验
如果一次交互收集了大量 Duration,并按从小到大排序:
- P50:中位数,约一半样本不超过该值;
- P95:约 95% 样本不超过该值,剩余长尾更慢;
- P99:约 99% 样本不超过该值,用于观察更极端长尾。
9.1 为什么平均值不够
假设大多数用户很快,少量低端设备极慢,Average 可能看起来正常,却掩盖严重体验。P50 描述典型情况,P95/P99 帮助发现长尾,但它们也不能说明慢的原因,需要按维度分群。
总体 P95 变差,可能只是新版本用户结构改变,也可能是某个地区后端异常。必须比较相同 Segment。
9.2 分位数也会误导
- 10 次 Lab 运行无法可靠估计 P99;
- 低流量页面的 P95/P99 会剧烈波动;
- 不同设备组的 P95 不能直接取平均得到总体 P95;
- Sampling Policy 改变会改变分布;
- Timeout、页面关闭和失败样本若被丢弃,会让结果虚假变好;
- Simpson's Paradox 可能让总体趋势与分群趋势相反。
聚合系统应基于原始样本、Histogram 或可合并的 Quantile Sketch,而不是平均各节点已经计算好的百分位数。
9.3 如何选择观察层级
- P50:日常典型体验与大盘变化;
- P75/P90/P95:核心 Journey 的稳定长尾;
- P99:高流量、高价值且样本充足的极端体验;
- Max:常受异常值影响,适合排查,不适合单独决策。
具体使用哪个分位数应由流量、业务风险和误差容忍度决定,不是数值越高越专业。
十、单变量实验:从相关性走向因果证据
性能 Trace 告诉你“发生了什么”,但修改能否改善问题仍需实验验证。
10.1 先写假设
不合格的假设:
给组件加 memo 应该会更快。可验证的假设:
订单筛选时,备注输入状态位于页面根组件,导致 5000 行表格参与每次 Commit。
如果把备注状态下沉到侧栏,在数据、设备和操作脚本不变时,
OrdersTable 的 actualDuration 和输入到 Commit 的 P95 应下降,
且结果正确性、内存和列表筛选耗时不恶化。假设明确了根因、修改、目标指标和 Guardrail。
10.2 一次只改变一个主要因素
如果同时执行:
- State Colocation;
memo;- 虚拟化;
- 数据结构重写;
- Bundle 拆分;
即使整体变快,也无法知道收益来自哪里,未来也无法判断哪项复杂度值得保留。应按假设逐步实验,每步保存基线。
“单变量”指一次验证一个主要因果假设,不代表代码只能改一行。实现一个状态所有权调整可能涉及多个文件,但不应同时混入无关优化。
10.3 固定实验条件
至少固定:
- Git Commit、依赖和 Build Mode;
- Browser、OS、设备、电源与温控状态;
- CPU/Network Profile;
- 数据集、账号、Feature Flag;
- Cache、Service Worker 和登录状态;
- 操作脚本与输入节奏;
- 后端环境和测试时段;
- 预热轮数、正式轮数和异常处理规则。
10.4 同时测收益和代价
| 目标优化 | 可能的代价 |
|---|---|
| Memoization | 比较成本、内存、旧闭包和维护复杂度 |
| Prefetch | 流量、QPS、隐私 Cache 和电量 |
| Virtualization | 可访问性、查找、动态高度与滚动恢复 |
| Worker | 序列化、复制、调度与错误处理 |
| Code Splitting | Chunk Waterfall、失败和部署版本兼容 |
| Cache 扩大 | 内存、陈旧数据和账号隔离 |
优化目标指标改善,但 Error Rate、Memory 或业务结果变差,不能直接判定成功。
十一、性能回归:让一次收益成为长期能力
性能回归治理通常分三层:
11.1 静态预算适合硬门禁
相对稳定、容易重复的指标适合直接阻止合入:
- Initial/Route JS 与 CSS Size;
- 未压缩图片或字体体积;
- Chunk 数量和重复依赖;
- 明确禁止的同步资源;
- Bundle 中意外引入的大型库。
静态预算仍要理解用户价值。一个小 Bundle 也可能执行很慢,一个大但延迟加载的 Route Chunk 未必影响首屏。
11.2 高噪声时间指标先做趋势与告警
共享 CI 机器上的 Navigation Timing、Render Duration 和 Long Task 容易受调度噪声影响。更稳妥的做法:
- 使用固定 Runner 或专用设备;
- 多轮运行并保留分布;
- 同一 Job 中比较 Baseline 与 Candidate;
- 同时设置绝对阈值和相对退化阈值;
- 对极小基线避免只用百分比;
- 超标先生成 Trace Artifact,再决定阻断或人工审核;
- 持续跟踪趋势,避免每次更换环境都重置基线。
时间基准足够稳定后可以升级为门禁,但不能把偶发失败简单标记为 Flaky 后长期忽略。
11.3 发布后使用同分群验证
比较版本时应保持 Route、Journey、设备、网络、地区和缓存等 Segment 可比。发布结构变化、节假日流量和用户群改变都会影响总体分位数。
建议记录:
type PerformanceSampleContext = {
appVersion: string;
routeId: string;
journeyId: string;
deviceTier: 'low' | 'mid' | 'high';
networkTier: 'slow' | 'regular' | 'fast' | 'unknown';
cacheState: 'cold' | 'warm' | 'unknown';
visibilityState: DocumentVisibilityState;
};这些字段必须保持低基数并尊重隐私。版本发布后若目标 Segment 的 P95 恶化,还应同时观察 Error、Crash、后端延迟和 Feature Flag,不能立即把相关性归因到某个前端 Commit。
十二、完整案例:订单筛选为什么卡顿
以下是方法示例,不提供虚构的优化结果。
12.1 定义问题
RUM 显示订单列表筛选 Journey 的长尾恶化,主要集中在低端和中端设备、大数据账号。先固定与该 Segment 接近的 Lab 条件:
- Production Build;
- 固定浏览器版本与 CPU Throttling;
- 5000 条确定性订单数据;
- 页面和 Query Cache 已预热;
- 自动化输入相同筛选词;
- 每轮记录 User Timing、Profiler 和 Trace。
12.2 建立阶段证据
- Performance Trace 先检查 Event Queue、Long Task、Script、Layout 和 Paint;
- 若主时间在 React Render 区间,再打开 React Profiler;
- Profiler 检查哪些组件参与慢 Commit,以及触发原因;
- 检查 DOM 数量、Layout 和 GC,避免只盯 React;
- 将 Trace 时间轴与
orders-filter:start、commit标记对齐。
12.3 按假设逐项验证
每个实验都比较:
- 用户输入到目标里程碑的 Duration 分布;
- React
actualDuration与 Commit 数量; - Long Task、Layout、Paint;
- Heap 与 GC;
- 结果正确性和可访问性。
只有目标指标在多轮实验中稳定改善、Guardrail 正常,并在发布后同 Segment RUM 中复现,才能把修改归为有效优化。
十三、常见误区与错误案例
13.1 在 Development Build 下比较生产性能
开发模式包含额外检查、警告、Source Map 和 Strict Mode 行为。它适合定位逻辑,不适合作为生产耗时结论。使用 Production 或专用 Profiling Build,并在目标设备复测。
13.2 只看 React Render 次数
一次廉价 Render 可能没有 DOM Mutation,减少次数也不一定改善用户体验。应同时看 Duration、Commit、Layout、Paint 和用户 Journey。
13.3 只跑一次并取最好结果
最好结果通常反映幸运的 Cache、JIT 和系统调度。应预热、多轮交替运行,报告分布和异常规则。
13.4 用 Average 掩盖长尾
Average 无法说明低端设备和弱网用户。至少观察 P50 与适合流量规模的长尾分位数,并按设备、网络、Route 和版本分群。
13.5 用少量 Lab 数据计算 P99
样本不足时 P99 几乎等同于最大值,波动巨大。Lab 更适合多轮 Median、P75/P90 或置信区间;P99 通常需要高流量 RUM 或大量稳定样本。
13.6 CPU Throttling 等于真实低端机
节流只建立近似压力环境,不能模拟内存、GPU、温控和系统调度。最终结论必须经过代表性真机验证。
13.7 一次提交混入多个优化
结果变快却无法证明原因,也无法评估每项复杂度是否值得。按主要假设拆分实验,保留每一步基线和 Trace。
13.8 只守 Bundle Size,不守执行成本
相同字节量的 JavaScript 解析和执行成本可能不同,延迟加载也会改变影响路径。资源预算必须与用户时间和主线程预算组合。
13.9 线上指标改善就直接认定因果
发布时用户结构、后端、缓存和流量可能同时变化。使用 Canary、Feature Flag、相同 Segment 和单变量发布提高因果可信度。
十四、性能实验报告模板
# Experiment: orders-filter-state-colocation
## Problem
- Journey:
- Affected segment:
- User-visible symptom:
## Environment
- Commit / build:
- Browser / OS / device:
- CPU / network:
- Cache / data set:
- Script / repetitions:
## Baseline
- User metric distribution:
- React Profiler evidence:
- Browser Trace evidence:
- Guardrails:
## Hypothesis
- Suspected cause:
- Single primary change:
- Expected metric movement:
## Result
- Before / after distribution:
- Trace artifact:
- Functional and resource guardrails:
- Remaining uncertainty:
## Decision
- Keep / revert / continue experiment:
- Regression budget:
- Owner:模板的价值是让结论可审查、可重复。没有环境、基线和证据的“优化了 40%”无法判断测量对象、样本和因果关系。
十五、工程检查清单
指标与预算
- 是否从 User Journey 而不是工具指标出发;
- Budget 是否绑定 Route、设备、网络、缓存和分位数;
- 是否同时包含用户结果、执行/资源代理和 Guardrail;
- 是否明确超时、失败和中途离开样本的统计规则。
采集与定位
- 是否结合 Field 分布与 Lab 复现;
- 是否使用 Production 或专用 Profiling Build;
- React Profiler 是否只用于解释 React 阶段;
- Performance Trace 是否覆盖事件、脚本、Layout 与 Paint;
- User Timing 名称和终点语义是否清楚且不泄露隐私。
实验与回归
- 是否写出可证伪的根因假设;
- 是否一次验证一个主要变量;
- 环境、数据、Cache 和脚本是否固定;
- 是否多轮运行并报告分布;
- 是否保存 Trace、Profiler 和 Build 信息;
- CI 门禁是否考虑指标噪声;
- 发布后是否回到相同 RUM Segment 验证;
- 性能收益是否进入长期 Budget 和负责人体系。
十六、总结
React 性能工作的核心不是某个 Hook,而是一套证据方法:
- 用 User Journey 定义用户真正等待的起点和终点;
- 用 Performance Budget 把体验、执行成本和资源成本变成工程约束;
- 用 Field Data 找到真实受影响人群,用 Lab Data 稳定复现;
- 用 React Profiler 判断组件树做了多少工作;
- 用 Browser Performance Panel 判断主线程、网络和渲染管线如何消耗时间;
- 用 CPU/Network Throttling 建立可重复压力条件,再用代表性真机确认;
- 用 P50/P95/P99 理解典型与长尾,而不是依赖平均值;
- 用单变量实验验证因果,并观察正确性、内存和流量 Guardrail;
- 用静态预算、Lab 趋势和 RUM 分层监控防止性能回归。
只有当问题可复现、指标可解释、根因有 Trace、修改可归因、结果能在真实用户中复现,并且后续版本会自动发现回退时,性能优化才真正完成。
问答复盘
Q1:为什么不能看到组件重复 Render 就直接加 memo?
答: Render 次数不是用户指标。组件可能很廉价且没有 DOM 变化,真正成本也可能在 Layout 或 Paint;应先用用户 Journey 和 Trace 证明 React Render 是主要瓶颈。
Q2:Performance Budget 为什么必须包含设备、网络和缓存条件?
答: 同一页面在冷缓存、弱网、低端设备与热缓存桌面上的分布完全不同。没有条件的阈值无法复现,也无法判断超标对应哪类用户。
Q3:Field Data 与 Lab Data 哪个更可信?
答: 两者回答不同问题。Field 反映真实用户分布和长尾,Lab 提供受控复现和完整调试证据;可靠结论通常需要两者闭环。
Q4:actualDuration 下降是否证明用户已经感到更快?
答: 不能。它主要描述 React 子树本次 Render 成本,不包含完整排队、网络、Layout、Paint 和视觉完成时间,必须与用户指标和浏览器 Trace 联合验证。
Q5:CPU 4x Throttling 能否代表某款低端手机?
答: 不能精确代表。它适合在同一环境放大 CPU 问题和做相对比较,但无法模拟真实设备的架构、内存、GPU、温控和系统调度。
Q6:为什么不能平均多个服务节点各自计算的 P95?
答: Percentile 不是可直接平均的统计量。各节点样本量和分布不同,应聚合原始样本、Histogram 或可合并 Quantile Sketch 后重新计算总体分位数。
Q7:P99 是否总比 P95 更有价值?
答: 不是。P99 需要更多样本,低流量场景会非常不稳定。应按业务风险、流量和误差容忍度选择分位数,而不是盲目追求更高数字。
Q8:一次修改同时加入 Memoization 和 Virtualization,整体变快,能否直接合入?
答: 功能上可以评估,但无法证明各自收益与代价。性能工程应拆成单变量实验,否则难以维护、回退和建立准确预算。
Q9:哪些性能指标适合做 CI 硬门禁?
答: Bundle Size、资源体积等确定性较高的指标更适合。高噪声时间指标应先在固定 Runner 上多轮比较、生成趋势和 Trace,稳定后再决定是否阻断。
Q10:怎样才算一次性能优化真正完成?
答: 需要同场景前后分布证明改善,功能和资源 Guardrail 正常,真实用户相同 Segment 复现收益,并建立自动预算或监控防止后续回归。
延伸知识
- Web Vitals:LCP、INP、CLS、TTFB、Field/Lab 与 Attribution;
- React 渲染性能:State Colocation、Selector、Stable Props 与 Profiler Flamegraph;
- 列表与大数据:Virtualization、Overscan、Dynamic Height 与 Worker;
- 资源与网络:Code Splitting、Preload、Prefetch、缓存和图片策略;
- 浏览器渲染:Long Task、Style、Layout、Paint、Composite;
- 统计方法:Histogram、置信区间、A/B Test 与异常检测。