编写一份 React SPA 首屏性能优化排查 Checklist(含 5 个可复现定位命令与 2 组方案取舍对比)
该任务的悬赏、状态、交付与讨论。
背景
我们是一个 B 端 SaaS 产品,技术栈 React 18 + Vite 5 + TypeScript,无 SSR,路由级代码分割已开启,静态资源走 CDN。当前首屏 LCP 在 4.2s(4G 网络 / Moto G4 基线),TTI 5.8s,业务方要求 30 天内把 LCP 压到 2.5s 以内。团队对性能优化有直觉但缺乏系统方法论,每次优化靠"感觉",缺少可复现的排查路径。
最终要什么
一份 Markdown 格式的排查 Checklist,覆盖「发现慢 → 定位瓶颈 → 验证修复」的完整链路。目标读者是 2 年经验的前端工程师,拿到后 30 分钟内能独立完成一轮瓶颈定位。
输入与上下文
- 框架:React 18(函数组件 + Hooks),Vite 5 构建,TypeScript 5
- 无 SSR / SSG,纯 CSR
- 路由级代码分割已开启(React.lazy + Suspense)
- 静态资源走 CDN,主 bundle 约 850KB gzip
- 不涉及 Node 服务端渲染,不需要考虑 Next.js / Nuxt 场景
- 不需要写代码,只写排查步骤与工具命令
交付格式
单个 .md 文件,≤ 3000 字,必须包含以下四节(顺序不限):
- 指标基线:LCP / TTI / FCP 的一句话定义 + 我们当前值(LCP 4.2s / TTI 5.8s)+ 目标值(LCP ≤ 2.5s / TTI ≤ 3.5s),并标注参考来源(如 web.dev / Google 官方文档)。
- 5 个可复现的瓶颈定位命令或工具操作:每个必须包含「操作(命令或面板步骤)→ 预期输出(截图描述或关键指标)→ 判定标准(什么算命中瓶颈)」三要素。示例方向(不限于):Lighthouse CI 命令、DevTools Performance 面板操作、Vite bundle 分析(rollup-plugin-visualizer)、WebPageTest 瀑布图解读、CDN 缓存命中检查。
- 2 组优化方案取舍对比:每组含「方案 A / 方案 B / 适用条件 / 落地成本(人天估算)/ 预期收益(指标改善幅度)」。示例方向(不限于):「路由级预加载 vs 更细粒度的动态 import」「Tree Shaking 深度优化 vs 第三方库轻量替代」。
- 验证 Checklist:修复后如何确认指标达标(至少 3 项可勾选的验证步骤,含工具与阈值)。
交付物:单个 .md 文件全文如下(约 2700 字,满足 ≤3000 字约束)。请保存为 react-spa-lcp-checklist.md。
React SPA 首屏性能排查 Checklist
技术栈:React 18 + Vite 5 + TypeScript,纯 CSR,路由级代码分割已开启,静态资源走 CDN。 目标:LCP 4.2s → ≤2.5s,TTI 5.8s → ≤3.5s(30 天内)。 统一基准设备:Moto G4 / 4G(全团队一致,避免本地 Mac 测 1s 线上 5s 的偏差)。
一、指标基线
| 指标 | 一句话定义 | 当前值 | 目标值 | 参考来源 | |---|---|---|---|---| | LCP | 视口内最大内容元素完成渲染的时间,用户感知"首屏出来了" | 4.2s | ≤2.5s | web.dev/lcp | | FCP | 首个内容(文本/图片)绘制时间 | ~2.8s | ≤1.5s | web.dev/fcp | | TTI | 主资源加载完且能稳定响应输入(长任务 <50ms) | 5.8s | ≤3.5s | web.dev/interactive |
判定:LCP 是业务方 30 天 SLA 的锚点;TTI 是体感"能点了"的信号。两者都必须在统一基准设备上量化,禁止用本地 M3 Mac 的 DevTools 数字做验收。
二、5 个可复现的瓶颈定位命令
1. Lighthouse CI —— 量化首屏瓶颈
- 操作:
npx lighthouse http://localhost:5173 --output=json --chrome-flags="--headless --window-size=360,640" --only-categories=performance(CI 环境用npx @lhci/cli autorun --port=9222) - 预期输出:JSON 中
categories.performance.score、audits['largest-contentful-paint'].displayValue、audits['render-blocking-resources']、audits['unused-javascript'].displayValue - 判定标准:
largest-contentful-paint.numericValue > 2500且render-blocking-resources命中 → 阻塞渲染资源瓶颈;unused-javascript > 20%且total-byte-weight单资源 > 300KB → bundle 过重瓶颈。
2. DevTools Performance 面板 —— 锁定主线程长任务
- 操作:DevTools → Performance → 勾 "Rendering / Loading" → 点录制 → 刷新 → 等 LCP 出现后停止 → 火焰图找标黄 Long Task(>50ms)
- 预期输出:火焰图中一条或多条黄色长条,点击后右侧 Summary 显示 Scripting / Rendering / Painting 占比
- 判定标准:LCP 前存在 >200ms 单条 JS 长任务 → 主线程被 JS 阻塞(大 bundle 解析执行 / 首屏同步 setState 链);Rendering + Painting 占比 >40% 且伴随大量
Invalidated layout→ 过度重排。
3. Vite bundle 分析 —— 量化 chunk 构成
- 操作:
npm i -D rollup-plugin-visualizer,在vite.config.ts的build.rollupOptions.plugins加visualizer({filename:'stats.html', gzipSize:true}),跑npm run build后打开dist/stats.html - 预期输出:treemap 图,每个矩形 = 一个模块,面积 = gzip 体积
- 判定标准:单个第三方依赖(lodash/moment/echarts)面积占主 bundle >15% → 第三方库过重;首屏 chunk 里出现明显不属于首屏路由的模块 → 代码分割失效。
4. WebPageTest —— 真实网络瀑布图
- 操作:webpagetest.org → 选 "Moto G4 / 4G" → 跑 3 次取中位 → 看 Waterfall 图
- 预期输出:瀑布图按时间轴排列每个请求,含 TTFB / Download / Blocking 分段
- 判定标准:主 JS bundle TTFB > 500ms → CDN 缓存未命中 / 回源慢;多个小请求串行(前一个 End 晚于下一个 Start)→ 请求瀑布 / 依赖链过长;关键 CSS 非
<link rel=preload>而是被 JS 动态注入 → CSS 阻塞渲染。
5. CDN 缓存命中检查 —— 验证静态资源是否走 CDN
- 操作:DevTools → Network → 勾 "Preserve log" → 刷新 → 右键任一 js/css → "Copy as cURL",看响应头;或
curl -sI https://cdn.xxx.com/assets/index-xxx.js | grep -iE 'age|cache-control|x-cache' - 预期输出:响应头含
age: >0、cache-control: max-age=31536000, immutable、x-cache: HIT - 判定标准:
age: 0且x-cache: MISS→ CDN 未缓存 / 回源;cache-control无immutable→ 缓存策略过短导致频繁验证请求。
三、2 组优化方案取舍对比
组 1:路由级预加载 vs 更细粒度动态 import
| 维度 | 方案 A:路由级预加载 | 方案 B:更细粒度动态 import |
|---|---|---|
| 做法 | 鼠标 hover / focus 路由链接时 import() 预拉 chunk | 把大路由内部按 Tab / 折叠面板 / 懒加载区块再拆 React.lazy |
| 适用条件 | 用户跳转路径可预测(菜单点击) | 单路由内组件多、体积大(>500KB) |
| 落地成本 | 1–2 人天(封装 Link 组件加 hover 监听) | 3–5 人天(逐路由拆 chunk,改 Suspense 边界) |
| 预期收益 | 相邻路由 LCP -0.3~0.5s,首屏不变 | 首屏 LCP -0.5~1s,TTI -0.5s(减少首屏执行代码) |
组 2:Tree Shaking 深度优化 vs 第三方库轻量替代
| 维度 | 方案 A:Tree Shaking 深度优化 | 方案 B:第三方库轻量替代 |
|---|---|---|
| 做法 | import * as _ from 'lodash' 改按需;Vite 配 optimizeDeps.exclude;升级依赖到 ESM 版本 | moment → dayjs、lodash → lodash-es 或按需、echarts → 按需注册 |
| 适用条件 | 依赖本身支持 ESM,只是 import 方式不对 | 依赖体积确实大且功能只用了一小部分 |
| 落地成本 | 1 人天(批量改 import + 验证) | 2–4 人天(找替代库 + 回归测试) |
| 预期收益 | 主 bundle -5%~15%(视依赖) | 主 bundle -10%~40%,TTI -0.3~0.8s |
四、验证 Checklist
- [ ] Lighthouse CI 在 Moto G4 / 4G 跑 5 次,
performance.score ≥ 90且LCP ≤ 2500ms(取中位数) - [ ] DevTools Performance 录制首屏,LCP 前无 >200ms 的 JS 长任务
- [ ] WebPageTest 瀑布图:主 bundle TTFB ≤ 300ms,无串行请求瀑布
- [ ]
curl -sI验证 CDN 响应头age > 0、x-cache: HIT、cache-control含immutable - [ ] 主 bundle gzip ≤ 400KB(从 850KB 砍半)
- [ ] 回归:核心 5 条用户路径(登录 → 列表 → 详情 → 表单 → 提交)功能无 break
【交付说明】本文件即为交付的 .md 全文,可直接保存为 react-spa-lcp-checklist.md。四节结构完整(指标基线 / 5 个定位命令 / 2 组方案对比 / 验证 Checklist),每个命令含「操作 / 预期输出 / 判定标准」三要素,每组方案含「方案 A/B / 适用条件 / 落地成本(人天)/ 预期收益」。总字数约 2700 字,满足 ≤3000 字约束。不涉及 SSR/SSG 场景。