首页  |  学习总览  |  ← 上一章:状态管理 阶段七 · 第 7 章,共 10 章
阶段七 · 预计 70 小时

性能优化实战

性能优化最怕「凭感觉瞎调」。本章先建立指标与预算体系,再按「加载 → 渲染 → 运行时」三条链路逐个击破,最后落地监控与回归机制。学完本章,你能完成一次「发现问题 → 定位根因 → 优化 → 数据验证」的完整闭环。

1先建立指标:没有度量就没有优化

性能工作从「能说出数字」开始。

1.1 核心 Web 指标(背下来)

指标全称/含义衡量良好目标
FCPFirst Contentful Paint第一个内容(文字/图片)出现≤ 1.8s
LCPLargest Contentful Paint最大内容(主视觉/标题)渲染≤ 2.5s
INPInteraction to Next Paint交互响应(新一代核心指标,替代 FID)≤ 200ms
CLSCumulative Layout Shift布局稳定性(页面跳动程度)≤ 0.1
TBTTotal Blocking Time主线程被长任务阻塞的总时长≤ 200ms
💡 优化的优先级建议 首屏(FCP/LCP)→ 交互(INP)→ 稳定性(CLS)。先解决「用户第一眼和第一次点击」,收益最大。

1.2 性能预算(Performance Budget)

  • 体积预算:首屏 JS ≤ 200KB(gzip 后)、总 JS ≤ 400KB。
  • 时间预算:LCP ≤ 2.5s、INP ≤ 200ms。
  • 落地:CI 里用体积分析工具对比每次构建,超预算直接构建失败——性能问题在合并前被拦住。
2加载链路优化

目标:让首屏资源「更少、更小、更早、更合理缓存」。

2.1 代码分割与懒加载

// 路由级代码分割(每个路由一个 chunk,进入才加载)
const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));

// 组件级懒加载(弹窗/图表等大组件,用到才加载)
const Chart = lazy(() => import("./components/BigChart"));

检查工具:构建后用体积分析插件看每个 chunk 的大小,揪出「体积刺客」(比如整个图表库被塞进首屏)。

2.2 资源优化

  • 图片:优先 WebP/AVIF(同质量体积小 30%~50%);装饰图用 CSS/SVG;图标用字体图标或雪碧图。
  • 懒加载图片<img loading="lazy">,视口外图片不加载。
  • 响应式图片srcset 按屏幕宽度选不同尺寸图片,手机不加载桌面大图。
  • 字体font-display: swap 避免文字不可见;只加载需要的字重;中文字体用子集化。
  • 依赖体检:定期用体积分析工具找大依赖,能换轻量的就换(如日期库 dayjs 替代 moment)。
// 响应式图片示例
<img
  src="img-800w.jpg"
  srcset="img-400w.jpg 400w, img-800w.jpg 800w, img-1600w.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 800px"
  loading="lazy"
>

2.3 预加载策略(让浏览器提前干活)

// preload:提前加载当前页面立即要用的关键资源(首屏大图、字体)
<link rel="preload" href="hero.webp" as="image">

// prefetch:空闲时提前拉取「下一步可能访问」的资源(下一页路由)
<link rel="prefetch" href="/about">

// preconnect:提前建立第三方源连接(CDN、接口域名)
<link rel="preconnect" href="https://api.example.com" crossorigin>

2.4 缓存体系(阶段三的落地)

  • 静态资源:文件名带哈希 + 长缓存(一年),内容变则 URL 变自动拉新。
  • HTML:no-cache 每次协商,保证入口最新。
  • 接口:GET 类数据用请求缓存库缓存;敏感数据绝不进 CDN。
  • 离线兜底(进阶):Service Worker 缓存壳子,弱网秒开。
3渲染链路优化

目标:让页面「不卡」——减少无效渲染与主线程占用。

3.1 减少无效渲染

// 1. memo 组件:props 没变不重渲染
const Item = memo(function Item({ data, onSelect }) {
  return <li onClick={() => onSelect(data.id)}>{data.name}</li>;
});

// 2. 状态选择器:只订阅自己需要的部分(阶段六 Zustand 选择器)
const price = useStore(s => s.price);

// 3. 列表项抽组件 + key 稳定:列表更新时只 diff 变化的项

3.2 大列表:虚拟滚动

渲染 1 万个 DOM 节点必然卡。虚拟滚动的思路:无论列表多长,只渲染可视区 + 缓冲区的几十行,滚动时更新偏移。

// 成熟库:react-window(轻量)或 react-virtualized(功能全)
import { FixedSizeList as List } from "react-window";

<List
  height={600}              // 可视区高度
  itemCount={100000}        // 总条数 10 万
  itemSize={40}             // 每行高度(固定)
  width="100%"
>
  {({ index, style }) => (
    <div style={style}>第 {index} 行</div>   // style 由库计算位置
  )}
</List>;

3.3 骨架屏与占位

  • 数据加载中用骨架屏(灰色块模拟布局),比转圈体验好得多——用户「先看到结构,内容到了再填充」。
  • 图片加载中给固定宽高(防止 CLS)或模糊占位(LQIP)。
  • 配合 Suspense + 流式渲染,实现「边下载边显示」。
4运行时与内存优化

卡顿的根源:主线程被长任务占用、内存持续增长。

4.1 长任务拆分

主线程单次执行超过 50ms 就是长任务(Long Task),期间用户交互无响应。大计算拆成小片,用空闲时间处理:

// 思路:分批处理,每批让出主线程
async function processBigData(items) {
  const BATCH = 100;
  for (let i = 0; i < items.length; i += BATCH) {
    processBatch(items.slice(i, i + BATCH));
    await new Promise(r => setTimeout(r, 0));  // 让出主线程给交互
  }
}

// 更优雅:requestIdleCallback 空闲时做(低优先级)
requestIdleCallback(() => preloadNextPage());

4.2 Web Worker:把重活挪出主线程

// 计算密集任务(图片处理、数据清洗、加密)丢给 Worker
const worker = new Worker("./calc-worker.js");
worker.postMessage({ type: "process", data: bigData });  // 发任务
worker.onmessage = (e) => setResult(e.data);             // 收结果

注意:Worker 里不能操作 DOM,通过消息通信;只适合 CPU 密集任务(I/O 密集用异步就够了)。

4.3 内存泄漏排查

典型泄漏源与排查手段:

泄漏源例子排查/修复
定时器未清理setInterval 组件卸载后还在跑useEffect 清理函数 clearInterval
事件监听未移除window 上监听不卸载监听处返回解绑函数,卸载时执行
闭包持有大对象回调里引用了大数组用后置 null,或避免长期持有
全局缓存无限增长缓存 Map 只加不删限制缓存大小,LRU 策略

排查工具:DevTools Memory 面板「记录快照 → 操作 → 再快照 → 对比」,找持续增长的对象。

5监控与治理:让性能不退化

一次优化不难,难的是「持续不退化」。

5.1 三层监控体系

  • 实验室监控(Lab):Lighthouse CI 每次构建跑分,卡阈值。
  • 真实用户监控(RUM):线上用 PerformanceObserver 收集真实用户的核心指标,上报看板。真实环境才代表真实体验。
  • 回归对比:每次发版对比核心指标,劣化立即告警回滚。
// RUM 收集核心指标(简版)
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    report({ name: entry.name, value: entry.value });  // 上报到监控平台
  }
}).observe({ type: "largest-contentful-paint", buffered: true });
💡 性能优化的正确姿势(总结) ① 先量化(指标 + 预算);② 再定位(DevTools / 监控定位瓶颈在加载、渲染还是运行时);③ 用数据验证(优化前后对比);④ 上机制防退化(CI 门禁 + RUM 监控)。永远别「为了优化而优化」——没有数据支撑的优化都是玄学。
6实战项目(通关必做)

本章的实战就是「对自己项目动手」。

项目一:性能优化闭环

对自己任一项目:① 用 Lighthouse 跑分记录基线;② 做至少 5 项优化(代码分割、图片、缓存、memo、骨架屏…);③ 优化后再跑分并记录每项的变化;④ 写一份「优化报告」:问题 → 方案 → 数据对比。这份报告就是你面试的实锤素材。

项目二:内存泄漏排查实战

故意在项目里埋两个泄漏(定时器、事件监听),用 Memory 面板的快照对比法找出来并修复。记录完整的排查步骤。

7通关自检题

性能题面试必考,务必能「说出数字 + 说出手段」。

1. 核心 Web 指标有哪些?各自良好阈值是多少?

2. 什么是性能预算?怎么落地到 CI?

3. 代码分割的两种方式?怎么做?

4. 图片优化有哪些手段?srcset 是干什么的?

5. preload / prefetch / preconnect 的区别?

6. 虚拟滚动的原理?什么时候该用?

7. 长任务是什么?怎么拆?requestIdleCallback 干什么?

8. Web Worker 适合什么任务?不能做什么?

9. 内存泄漏的四种常见来源?怎么排查?

10. Lab 和 RUM 的区别?为什么都要?

✅ 全部通关? 性能体系已建立。下一阶段《测试与代码质量》,把你的代码从「能跑」升级到「经得起长期演进」。