性能优化不是让 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 字段与面板能力应以项目锁定版本和目标浏览器为准。

核心结论

  1. 先定义用户场景和成功标准,再选择指标与工具;工具输出本身不是目标。
  2. Performance Budget 必须绑定 Route、Journey、设备、网络、缓存和分位数,不能只有一个全站平均值。
  3. User-centric Metrics 描述用户等待和交互结果,Bundle Size、Render Count 等只是不完整的诊断代理。
  4. Field Data 提供真实分布,Lab Data 提供受控复现;两者用途不同,不能互相替代。
  5. React Profiler 主要解释组件 Render 与 Commit 关系,不能测量完整网络、Style、Layout、Paint 和像素显示时间。
  6. Browser Performance Panel 用于观察主线程、网络、脚本、Layout、Paint 和交互时序,但不直接告诉你哪个 React State 设计错误。
  7. CPU 与 Network Throttling 是可重复压力条件,不是某台真实设备或网络的精确模拟。
  8. P50 表示典型体验,P95/P99 表示长尾;样本量、分群和采样方式决定分位数是否可信。
  9. 性能优化必须通过单变量实验验证因果关系,并同时观察功能、错误率、内存和流量等护栏。
  10. 性能回归应组合确定性预算、实验室趋势和线上 RUM,避免把高噪声单次计时直接设为硬门禁。
  11. 开发模式、Strict Mode、扩展和日志会改变测量结果,最终结论应来自生产构建或专用 Profiling Build。
  12. 任何优化都应保留基线、环境、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 预算不是永远不变的常量

新业务可能合理增加成本,浏览器和设备结构也会变化。调整预算时应记录:

  1. 原预算和实际分布;
  2. 新功能带来的用户价值;
  3. 为什么不能在原预算内实现;
  4. 哪些资源或指标被放宽;
  5. 后续偿还计划和负责人。

不要让每次超标都通过“临时提高阈值”解决,否则 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
长尾发现 强 依赖选定场景
发布前验证 有滞后 强

合理流程是:

  1. RUM 发现特定 Route、版本、设备或网络的分位数异常;
  2. Lab 使用接近该分群的条件复现;
  3. Profiler/Trace 定位根因并做单变量实验;
  4. 发布后回到同一 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 一次有效录制的步骤

  1. 使用生产构建或明确的 Profiling Build;
  2. 关闭无关标签页、扩展和开发日志;
  3. 固定数据集、缓存状态、CPU 与网络条件;
  4. 预热需要预热的代码路径;
  5. 开始录制后只执行目标交互;
  6. 停止录制,先定位用户事件和可见结果;
  7. 查看中间是否存在 Long Task、Script、Layout 或 Paint;
  8. 使用 Bottom-up/Call Tree 找主要 Self Time 与 Total Time;
  9. 保存 Trace,并记录 Build Commit、环境和操作脚本;
  10. 修改后在同样条件重复多轮。

录制时间过长会产生巨大 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 建立阶段证据

  1. Performance Trace 先检查 Event Queue、Long Task、Script、Layout 和 Paint;
  2. 若主时间在 React Render 区间,再打开 React Profiler;
  3. Profiler 检查哪些组件参与慢 Commit,以及触发原因;
  4. 检查 DOM 数量、Layout 和 GC,避免只盯 React;
  5. 将 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 与异常检测。