外观
前端性能优化系统学习文档
适合人群:已经能独立开发前端项目,希望从页面加载、交互响应、运行时渲染、构建产物、监控体系等角度系统提升性能能力的学习者。
学习目标:从理解性能指标开始,掌握测量工具、加载优化、资源优化、JavaScript 优化、渲染优化、框架优化、缓存策略、性能监控和团队性能治理。
核对时间:2026-06-30。本文以 Web Vitals / Core Web Vitals 当前官方口径为基础:LCP、INP、CLS 是核心用户体验指标,INP 已取代 FID 成为交互响应核心指标。
相关文档:本文是 前端架构师技术栈全景学习文档.md 中“性能与生产稳定性”能力的展开;性能监控可继续阅读 前端质量保障安全可访问性与可观测性系统学习文档.md,构建产物优化可继续阅读 前端工程化与构建发布系统学习文档.md。
目录
- 性能优化到底优化什么
- 性能指标体系
- 测量工具与分析方法
- 从输入 URL 到页面可交互
- 网络与传输优化
- HTML、CSS 与字体优化
- 图片与媒体资源优化
- JavaScript 加载与执行优化
- 渲染性能优化
- 框架层性能优化
- 构建产物优化
- 缓存策略
- 运行时内存与长任务优化
- SSR、SSG、CSR 与性能取舍
- 性能监控与告警体系
- 性能预算与团队治理
- 常见性能问题排查清单
- 学习路线与练习
- 官方参考资料
1. 性能优化到底优化什么
前端性能优化不是把所有代码都“写得更快”,而是围绕用户体验和业务目标减少等待、卡顿、抖动和资源浪费。
常见目标:
- 页面更快展示核心内容。
- 用户点击、输入、拖拽后更快得到响应。
- 页面布局更稳定,不突然跳动。
- 首屏资源更小、更少、更合理。
- 低端设备也能流畅使用。
- 弱网环境下仍有可接受体验。
- 发布后能持续监控性能退化。
性能优化必须先测量,再定位,再优化。没有数据的优化很容易变成猜测。
1.1 性能优化的三类视角
用户视角:
- 什么时候看到内容?
- 点击后有没有反应?
- 页面是否乱跳?
- 滚动是否流畅?
技术视角:
- 网络请求是否太多?
- JS 是否太大?
- 主线程是否被长任务占满?
- 图片是否过大?
- 样式和布局是否频繁重算?
业务视角:
- 首屏慢是否影响转化?
- 卡顿是否影响下单、登录、搜索?
- 性能退化是否影响 SEO 和广告投放?
- 低端设备用户是否流失?
架构师要把三类视角连起来,不能只看 Lighthouse 分数。
2. 性能指标体系
2.1 Core Web Vitals
Core Web Vitals 是 Google / web.dev 推荐关注的一组核心用户体验指标。
| 指标 | 含义 | 良好阈值 |
|---|---|---|
| LCP | Largest Contentful Paint,最大内容绘制 | <= 2.5 秒 |
| INP | Interaction to Next Paint,交互到下一次绘制 | <= 200 毫秒 |
| CLS | Cumulative Layout Shift,累计布局偏移 | <= 0.1 |
这些阈值通常以真实用户访问数据的第 75 百分位来评估。也就是说,不是看一次本地跑分,而是看大多数真实用户的体验是否达标。
三项指标各自有完整的三档分级(以 web.dev 官方口径为准):
| 指标 | Good(良好) | Needs Improvement(需改进) | Poor(差) |
|---|---|---|---|
| LCP | <= 2.5s | 2.5s ~ 4.0s | > 4.0s |
| INP | <= 200ms | 200ms ~ 500ms | > 500ms |
| CLS | <= 0.1 | 0.1 ~ 0.25 | > 0.25 |
💡 为什么用第 75 百分位(P75)而不是平均值:性能分布是长尾的,平均数会被大量快加载拉低、掩盖慢体验;P75 表示"75% 的用户都比这个值快",是保证绝大多数用户体验的下限口径。CrUX / PageSpeed Insights / Search Console 的达标判定都是按 P75 计算的。
⚠️ 历史背景:Core Web Vitals 在 2022 年的第三项是 FID(First Input Delay)。由于 FID 只记录页面首次输入的延迟、且只测输入等待不测处理与渲染,Chrome 团队于 2024 年 3 月 12 日正式用 INP 取代 FID,FID 已从搜索信号与 Web Vitals 计划中弃用(过渡期内 PageSpeed Insights / CrUX 仍短暂展示 FID 数据,随后移除)。如果你的旧监控看板、旧文章或面试资料仍以 FID 为核心指标,需要更新为 INP。
2.2 LCP
LCP 衡量页面主要内容何时展示给用户。常见 LCP 元素:
- 首屏大图。
- 文章标题或正文块。
- 商品主图。
- 视频封面。
- 大的背景图。
按 Largest Contentful Paint API 规范,可被判为 LCP 候选的元素包括:<img>(GIF / APNG 取第一帧的呈现时间)、<video> 的 poster 或首帧(取更早者)、通过 CSS background-image: url() 加载的背景图(渐变不算)、SVG <image> 元素,以及包含文本节点或其他行内文本子元素的最大块级元素。
内部机制——LCP 的时间可以拆成四个阶段,优化时先定位瓶颈落在哪一段:
text
导航 ──► TTFB ──► 资源加载延迟 ──► 资源加载时间 ──► 渲染延迟 ──► LCP
服务器 LCP 资源开始 下载 + 解码 元素完成绘制
响应 下载前等待 所需时间 前的空窗| 阶段 | 常见成因 | 对应手段 |
|---|---|---|
| TTFB 慢 | 源站慢、无 CDN、SSR 渲染耗时 | 边缘缓存、优化服务端、减少重定向 |
| 资源加载延迟 | LCP 图片由 JS/CSS 注入才发现、懒加载误伤、关键 CSS 阻塞 | fetchpriority="high"、preload、避免 loading="lazy" 用于首屏图 |
| 资源加载时间长 | 图片过大、格式落后、带宽不足 | AVIF/WebP、响应式尺寸、CDN |
| 渲染延迟 | 主线程被 JS 长任务占住、字体等待 | 拆任务、延迟非关键 JS、font-display |
优化 LCP 的关键:
- 快速返回 HTML。
- 优先加载 LCP 图片。
- 减少阻塞渲染的 CSS / JS。
- 减少服务器响应时间。
- 使用合适的图片尺寸和格式。
- 避免客户端渲染让核心内容太晚出现。
2.3 INP
INP 衡量用户交互后,页面下一次视觉反馈的延迟。
交互包括:
- 点击。
- 输入。
- 键盘操作。
- 触摸操作。
INP 受主线程任务影响很大。长任务过多时,即使页面已经加载完成,用户点击按钮也可能迟迟没有反馈。
一次交互的三段延迟(INP breakdown)
INP 基于 Event Timing API,一次交互的总延迟由三段组成,优化前先确认瓶颈在哪一段:
text
用户开始交互 ──► 事件回调开始执行 ──► 回调执行完毕 ──► 下一帧绘制完成
│ input delay │ processing │ presentation │
│ (输入延迟) │ duration │ delay │
│ 主线程被其他任务 │ (事件处理) │ (呈现延迟) │
│ 占用导致的排队 │ 回调自身耗时 │ 样式/布局/绘制 │| 子阶段 | 慢的典型原因 | 优化方向 |
|---|---|---|
| Input delay | 加载期/交互前的长任务占住主线程、第三方脚本 | 拆分长任务、延后非关键 JS、yield |
| Processing duration | 事件回调本身同步计算太多、大范围重渲染 | 精简回调、只更新必要 DOM |
| Presentation delay | 回调触发的渲染工作过重(大 DOM、复杂选择器、强制同步布局) | 控制 DOM 规模、隔离渲染(contain)、避免布局抖动 |
💡 web-vitals 库的 onINP 回调在 attribution 模式下会返回 inputDelay、processingDuration、presentationDelay 三个字段,RUM 上报时把它们一起收集,线上定位 INP 问题就不用猜。
INP 如何取值
INP 观察页面整个生命周期内几乎所有 click、tap、键盘交互,最终报告的是其中最慢交互的延迟——交互数较少(约 50 次以内)的页面直接取最慢值,交互量更大的页面取约 98 百分位以忽略离群点(web-vitals 参考实现即此口径),而不是只测第一次输入——这正是它比 FID 更全面的原因。注意几个测量陷阱:
- 事件持续时间低于 104ms 的
event条目默认不进 PerformanceObserver(可用durationThreshold调整,最小 16ms),web-vitals已通过同时观察first-input兜底。 - iframe 内的交互不通过父页面上报,但计入用户真实体验,会造成 CrUX 与自建 RUM 的口径差异。
- 实验室环境模拟网络/CPU 可以近似测 INP,但交互是否发生依赖录制脚本,真实 INP 问题基本只能在 RUM 数据中发现。
优化 INP 的关键:
- 拆分长任务。
- 减少不必要 JS。
- 避免交互时同步执行大量计算。
- 把非关键任务延后。
- 使用 Web Worker 承担重计算。
- 降低框架渲染范围。
2.4 CLS
CLS 衡量页面生命周期内非预期布局偏移。
内部机制:CLS 不是所有偏移的简单累加,而是取最大 session window(会话窗口 burst)——一个 session window 定义为:相邻两次布局偏移间隔小于 1.0 秒、整个窗口总时长不超过 5 秒的一串偏移;把窗口内每次偏移的 layout shift score 相加,页面生命周期内所有窗口中得分最大的那个即为 CLS。这样设计是为了惩罚"连续爆发式跳动",同时不把整页浏览中零星的微小偏移无限累加。
单次偏移的 layout shift score 由两个因子相乘决定(Layout Instability API):
text
score = impact fraction × distance fraction
impact fraction:偏移前后两块"最大可视区域"重叠覆盖视口的比例
distance fraction:元素移动的最大距离 / max(视口宽, 视口高)也就是说,偏移越大、被推动的内容在视口中占比越高、离初始位置越远,扣分越重。只有非预期(start position 变化,即元素被推走)的偏移才计分;由用户在 500ms 内直接交互(点击、按键等)引发的偏移不计分。
常见原因:
- 图片没有设置宽高。
- 广告位后插入。
- 字体加载导致文本尺寸变化。
- 异步内容插入到现有内容上方。
- 骨架屏和真实内容尺寸不一致。
优化 CLS 的关键:
- 为图片、视频、广告保留尺寸。
- 使用
aspect-ratio。 - 避免在已有内容上方插入内容。
- 字体加载设置合理 fallback。
- 骨架屏尺寸接近真实内容。
2.5 其他重要指标
| 指标 | 含义 | 用途 |
|---|---|---|
| TTFB | Time to First Byte | 衡量服务器响应和网络链路 |
| FCP | First Contentful Paint | 首次内容绘制 |
| TBT | Total Blocking Time | 实验室环境衡量主线程阻塞 |
| Speed Index | 内容视觉展示速度 | 观察首屏加载节奏 |
| Long Tasks | 超过 50ms 的主线程任务 | 分析卡顿来源 |
| JS Heap | JS 堆内存 | 分析内存泄漏 |
几个容易混淆的点:
- TBT 与 INP 的关系:TBT 统计的是页面加载阶段(First Contentful Paint 到 Time to Interactive 之间)所有长任务超出 50ms 部分的总和,即"FCP 后到可交互前,主线程被阻塞的总时长"。Lighthouse 里 Good ≤ 200ms、需改进 200–600ms、差 > 600ms。因为实验室环境无法复现真实用户在整个页面生命周期里的全部交互,TBT 常被用作 INP 的实验室代理指标,但两者不是同一口径:TBT 只看加载期阻塞,INP 看的是每次交互的完整延迟。
- FCP 不等于内容可见:FCP 只要求"有任何内容"(一段文字、一张图片)被绘制;一张骨架屏也能拿到很快的 FCP。所以 FCP 快但 LCP 慢是常见组合,说明壳快了、主内容还没到。
- Long Tasks 的 50ms 阈值来源:源自 RAIL 性能模型——要让用户感知 100ms 内响应,事件处理预算约 50ms。超过 50ms 的任务就会占用这一帧处理输入的机会,是 input delay 的直接来源。
💡 现代浏览器提供了 Long Animation Frames API(LoAF)(Chrome/Edge 123 起正式支持),是 Long Tasks API 的升级版:它以"超过 50ms 的渲染帧"为单位,附带该帧内执行脚本的归因(script.invoker、sourceURL、堆栈位置),比 Long Tasks 只知道"发生了长任务"更适合定位 INP 元凶。web-vitals 的 INP attribution 就利用了 LoAF。
性能指标不是越多越好。团队应先围绕 LCP、INP、CLS 建立核心目标,再按问题引入辅助指标。
3. 测量工具与分析方法
3.1 实验室数据与真实用户数据
实验室数据:
- Lighthouse。
- Chrome DevTools Performance。
- WebPageTest。
- 本地模拟网络和 CPU。
优点:可重复、便于调试、适合开发阶段定位。
缺点:不代表所有真实用户环境。
真实用户数据:
- Chrome UX Report。
- PageSpeed Insights field data。
web-vitals上报。- Sentry / Datadog / New Relic / OpenTelemetry。
- 自建 RUM 系统。
优点:反映真实用户。
缺点:需要流量、采样、清洗和归因。
3.2 Chrome DevTools Performance
使用流程:
- 打开 Performance 面板。
- 选择 CPU throttling 和 Network throttling。
- 开始录制。
- 刷新页面或执行交互。
- 停止录制。
- 分析 Main thread、Network、Timings、Screenshots。
重点观察:
- 主线程是否有长任务。
- Long Task 由哪个脚本引起。
- Layout / Recalculate Style 是否频繁。
- 图片、字体、JS 请求顺序是否合理。
- LCP 元素是什么。
- 交互后是否有大量同步计算。
3.3 Lighthouse
Lighthouse 适合快速发现问题:
- 性能分数。
- LCP / TBT / CLS。
- 图片优化建议。
- 未使用 JS / CSS。
- 阻塞渲染资源。
- 缓存策略。
- 可访问性和 SEO。
但不要只追求 100 分。业务复杂应用可能不必追求满分,而应围绕真实用户和关键链路达标。
3.4 性能分析步骤
推荐流程:
- 明确页面与用户场景。
- 明确指标目标。
- 收集实验室数据。
- 收集真实用户数据。
- 定位最大瓶颈。
- 只改一个变量。
- 对比优化前后。
- 固化为规则或预算。
不要一次性做大量优化,否则很难判断哪个改动真正有效。
4. 从输入 URL 到页面可交互
理解浏览器加载流程是性能优化基础。
4.1 大致过程
- DNS 解析。
- 建立 TCP / TLS 连接。
- 发送 HTTP 请求。
- 服务器返回 HTML。
- 浏览器解析 HTML。
- 发现 CSS、JS、图片、字体等资源。
- 下载和解析 CSS。
- 构建 DOM 和 CSSOM。
- 执行 JavaScript。
- 计算样式。
- 布局。
- 绘制。
- 合成。
- 页面响应用户交互。
4.2 关键渲染路径
影响首屏的关键资源包括:
- HTML。
- 阻塞渲染的 CSS。
- 阻塞解析的同步 JS。
- 首屏关键图片。
- 首屏字体。
优化首屏就是让关键资源更快、更少、更早到达。
4.3 阻塞关系
CSS 会阻塞渲染,因为浏览器需要样式信息才能正确绘制页面。
普通同步 script 会阻塞 HTML 解析:
html
<script src="/main.js"></script>可以使用:
html
<script defer src="/main.js"></script>defer 脚本会在 HTML 解析完成后按顺序执行,更适合大多数应用入口脚本。
async 脚本下载完成后立即执行,顺序不保证,适合独立第三方脚本。
三者的机制差异(HTML 规范定义):
| 属性 | 下载是否阻塞解析 | 执行时机 | 多脚本顺序 | 典型用途 |
|---|---|---|---|---|
| 无属性(同步) | 是:解析停住,等下载+执行完 | 立即 | 按文档顺序 | 极少用(如首屏前置检测) |
defer | 否:并行下载 | DOMContentLoaded 前、解析完成后 | 保持文档顺序 | 应用入口、依赖顺序的脚本 |
async | 否:并行下载 | 下载完成即执行(可能打断解析) | 谁先下载完谁先执行 | 埋点、独立第三方脚本 |
type="module" | 否 | 默认 defer 语义(解析后执行、保序) | 保持顺序 | 现代打包产物 |
💡 defer 的"保持顺序"是关键差异:入口脚本 app.js 依赖先加载的 vendor.js 时,用两个 defer 是安全的,用两个 async 则是竞态赌局。另外 async/defer 都只对外部 src 脚本有效,内联 <script> 加它们不生效——内联脚本要延后只能改成外部文件或动态注入。
5. 网络与传输优化
5.1 减少请求数量与关键请求深度
优化方向:
- 合理合并关键资源。
- 非关键资源延后加载。
- 删除无用依赖。
- 避免首屏请求瀑布流。
- SSR / SSG 中提前输出关键数据。
请求数量不是越少越好。在 HTTP/2 / HTTP/3 下,多路复用降低了请求开销,但关键链路上的请求依赖仍然会拖慢页面。
5.2 压缩
常用压缩:
- Brotli:通常比 gzip 压缩率更好。
- gzip:兼容性广。
适合压缩:
- HTML。
- CSS。
- JS。
- SVG。
- JSON。
不适合重复压缩:
- 已压缩图片。
- 视频。
- zip 文件。
5.3 CDN
CDN 的价值:
- 靠近用户。
- 降低源站压力。
- 提升静态资源命中。
- 支持边缘缓存。
- 支持 HTTP/2 / HTTP/3、Brotli、TLS 优化。
实践:
- 静态资源文件名带 content hash。
- 设置长期缓存。
- HTML 设置较短缓存或协商缓存。
- 发布时保证资源不可变。
5.4 Resource Hints
dns-prefetch
html
<link rel="dns-prefetch" href="https://cdn.example.com">提前解析 DNS。
preconnect
html
<link rel="preconnect" href="https://cdn.example.com" crossorigin>提前建立连接,适合首屏关键第三方源。
preload
html
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">告诉浏览器这个资源很快会用到。
prefetch
html
<link rel="prefetch" href="/next-page.js">适合预取未来可能需要的资源,不适合当前首屏关键资源。
5.5 避免滥用预加载
预加载不是越多越好。滥用会挤占关键资源带宽,反而让页面更慢。
原则:
- 只 preload 当前页面马上需要的关键资源。
- preconnect 只给少数关键域名。
- prefetch 用于用户下一步大概率访问的资源。
6. HTML、CSS 与字体优化
6.1 HTML 优化
HTML 是浏览器发现资源的起点。
优化方向:
- 服务器尽快返回 HTML。
- SSR / SSG 输出首屏核心内容。
- 避免 HTML 中内联过大 JSON。
- 关键 meta、preload、script defer 放置合理。
- 不把大量非首屏内容阻塞在 HTML 中。
6.2 CSS 优化
CSS 优化方向:
- 删除未使用 CSS。
- 拆分首屏关键 CSS 与非关键 CSS。
- 避免过深选择器。
- 避免频繁触发布局的样式变化。
- 使用 CSS containment 隔离复杂区域。
- 避免大范围
box-shadow、filter、backdrop-filter滥用。
6.3 Critical CSS
Critical CSS 指首屏必需样式。
适合:
- 营销页。
- 文档页。
- 静态内容页。
- 电商商品详情页。
不一定适合:
- 大型 SPA 后台系统。
- 动态主题复杂的应用。
6.4 字体优化
字体常见问题:
- 字体文件过大。
- 字体阻塞文本显示。
- 字体加载后文本尺寸变化导致 CLS。
优化方法:
css
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-display: swap;
}建议:
- 使用 WOFF2。
- 子集化字体。
- 控制字重数量。
- 对关键字体 preload。
- 使用合适 fallback,减少布局变化。
7. 图片与媒体资源优化
7.1 图片格式
常见格式:
| 格式 | 适用场景 |
|---|---|
| JPEG | 照片,兼容性好 |
| PNG | 透明图、截图、简单图形 |
| WebP | 通用压缩,兼容性较好 |
| AVIF | 更高压缩率,适合现代浏览器 |
| SVG | 图标、矢量图、简单插画 |
7.2 响应式图片
html
<img
src="/product-800.webp"
srcset="/product-400.webp 400w, /product-800.webp 800w, /product-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, 50vw"
width="800"
height="600"
alt="Product"
>关键点:
- 提供多个尺寸。
- 正确设置
sizes。 - 设置
width和height防止 CLS。 - 不给移动端下载桌面大图。
7.3 懒加载
html
<img src="/below-fold.webp" loading="lazy" alt="Below fold">适合非首屏图片。
不要给 LCP 图片使用 loading="lazy",否则可能推迟首屏最大内容展示。
7.4 LCP 图片优先级
html
<img
src="/hero.avif"
width="1200"
height="675"
fetchpriority="high"
alt="Hero"
>也可以 preload:
html
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">7.5 视频优化
建议:
- 首屏不自动加载完整视频。
- 使用封面图。
- 设置
preload="metadata"或none。 - 多码率自适应。
- 非关键视频懒加载。
- 避免把大视频当背景特效滥用。
8. JavaScript 加载与执行优化
8.1 JS 成本不只是下载
JavaScript 的成本包括:
- 下载。
- 解压。
- 解析。
- 编译。
- 执行。
- 触发渲染。
- 占用内存。
同样大小的 JS,在低端手机上的成本远高于高性能电脑。
8.2 减少首屏 JS
方法:
- 路由级代码分割。
- 组件级懒加载。
- 删除未使用依赖。
- 使用更小替代库。
- 避免把后台管理系统整包加载到登录页。
- 把非关键逻辑延后。
React 示例:
tsx
import { lazy, Suspense } from "react";
const SettingsPage = lazy(() => import("./SettingsPage"));
export function App() {
return (
<Suspense fallback={null}>
<SettingsPage />
</Suspense>
);
}8.3 避免长任务
长任务指主线程执行超过 50ms 的任务。长任务会阻塞输入响应。
拆分任务:
js
async function processItems(items) {
for (let i = 0; i < items.length; i += 100) {
const chunk = items.slice(i, i + 100);
processChunk(chunk);
await new Promise((resolve) => setTimeout(resolve, 0));
}
}现代浏览器中也可以使用调度 API 或 requestIdleCallback,但要注意兼容性和任务优先级。
更推荐的现代写法是 scheduler.yield()(Task Scheduling API 的一部分,Chrome/Edge 129 起稳定):它同样把当前任务暂停、让浏览器处理挂起的输入和渲染,但与 setTimeout(0) 的关键区别是恢复执行时带优先级——你的续体排在其他新任务之前,不会被第三方脚本刚排队的任务插队,也不会受定时器最小延迟钳制(嵌套超过 4 层后 setTimeout 会被钳到 ≥4ms):
js
async function processItems(items) {
for (const item of items) {
processItem(item);
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
await new Promise((r) => setTimeout(r, 0)); // 回退方案
}
}
}各手段的取舍:
| 手段 | 机制 | 注意 |
|---|---|---|
setTimeout(0) | 排一个宏任务 | 被后续任务插队;深层嵌套(计时器嵌套层级 >5)会被规范钳制到 ≥4ms |
scheduler.yield() | 让出但保留续体优先级 | Safari 等暂不支持,需回退 |
postTask({priority:'user-blocking'}) | 显式声明任务优先级 | 支持面同上,需要 scheduler 检测 |
requestIdleCallback | 空闲期执行 | 触发时机不保证、可能长时间不来,不适合必达任务 |
⚠️ await / 微任务不能让出主线程:微任务队列在当前任务清空前会一直跑完,await Promise.resolve() 之后的代码仍属于同一渲染前的连续执行。想让浏览器"喘气"必须让出到宏任务级别(yield / setTimeout / rAF 链)。
8.4 Web Worker
适合放到 Worker:
- 大量数据计算。
- 图片处理。
- 加密解密。
- 大 JSON 解析后处理。
- 搜索索引。
不适合:
- 频繁访问 DOM。
- 小而频繁的轻量任务。
- 强依赖主线程 UI 状态的逻辑。
Worker 与主线程通信有序列化成本,不能无脑使用。
8.5 第三方脚本
第三方脚本常是性能黑洞:
- 统计。
- 广告。
- 客服。
- A/B 测试。
- 热力图。
- 埋点 SDK。
治理方法:
- 明确每个第三方脚本负责人。
- 非关键脚本延后加载。
- 使用
async或defer。 - 监控第三方脚本耗时。
- 定期清理不用的 SDK。
- 对第三方故障做降级。
9. 渲染性能优化
9.1 浏览器渲染流水线
常见步骤:
- JavaScript。
- Style。
- Layout。
- Paint。
- Composite。
每步的实际含义与失效方式:
| 阶段 | 浏览器做了什么 | 典型昂贵操作 |
|---|---|---|
| JavaScript | 执行脚本,通常是样式变更的触发源 | 大计算、频繁 DOM 读写 |
| Style | 将 CSS 规则匹配到 DOM 元素,计算每个元素的最终样式(Recalculate Style) | 复杂选择器、大量元素命中变更 |
| Layout | 根据盒模型计算元素的位置与尺寸 | 全局 reflow、布局深度大的表格 |
| Paint | 填充像素:文本、颜色、边框、阴影、渐变光栅化 | box-shadow、filter、border-radius 大面积重绘 |
| Composite | 把各图层(layer)按顺序、变换、透明度合成到屏幕 | 图层过多导致内存上升 |
优化思路是让变更尽早终止:
- 只改
transform/opacity时,跳过 Style 之后的 Layout 和 Paint,直接进 Composite(前提是元素已被提升为独立合成层)。 - 改
color、background等不影响力学的属性时,需要 Paint 但不需要 Layout。 - 改
width、top、字体大小等几何属性时,Layout → Paint → Composite 全量重走。
⚠️ 常见误区:"GPU 加速万能"。给元素加 will-change: transform 确实可能让它提升为合成层,使动画只走 Composite,但合成层不是免费的:每个层都要独立光栅化并占用显存。层爆炸(大量小图层、过度使用 will-change)会让移动端内存和光栅化成本反而上升。只对真正持续动画的元素提层。
💡 判断某次样式变更走到哪一步:DevTools → Performance → 看帧上的紫色(Layout)、绿色(Recalculate Style/Paint)、橙色(Composite)占比;或用 Rendering 面板的 Paint flashing(绿=重绘、蓝=重排区域)直接看哪些区域在刷新。
如果每一帧都做大量 JS、布局和绘制,就会掉帧。
60 FPS 下,一帧约 16.7ms。实际留给 JS 的时间更少。
9.2 避免强制同步布局
错误示例:
js
box.style.width = "200px";
const width = box.offsetWidth;
box.style.height = `${width}px`;浏览器可能被迫立刻计算布局。
内部机制:浏览器平时把布局结果缓存起来,同一帧内多次读 offsetWidth / getBoundingClientRect() 并不贵。但一旦 JS 修改了样式,缓存就失效;此时再读几何属性,浏览器必须立刻用当前样式重新跑一次 Layout 才能返回值——这就是"强制同步布局"(forced synchronous layout / layout thrashing)。上面的代码正是"写→读→写"的经典抖动模式,若发生在循环里,每轮都触发一次全页面布局,代价线性放大。
容易触发强制同步布局的读取属性:offsetTop/Left/Width/Height、clientWidth/Height、scrollTop/scrollLeft/scrollHeight、getComputedStyle()、getBoundingClientRect() 等。
更系统的解法是 FastPath DOM API:Element.checkVisibility()、IntersectionObserver(观察可见性/交叉)、ResizeObserver(观察尺寸变化,回调里读尺寸不会抖动)把"读几何信息"变成异步推送,避免手写轮询式读写。
💡 读写分离示例:
js
// 先集中读
const widths = items.map((el) => el.offsetWidth);
// 再集中写
items.forEach((el, i) => {
el.style.height = `${widths[i]}px`;
});优化思路:
- 批量读 DOM。
- 再批量写 DOM。
- 使用
requestAnimationFrame。 - 避免在循环中反复读取布局属性。
9.3 动画优化
优先使用:
transformopacity
谨慎使用:
widthheighttopleftbox-shadowfilter
示例:
css
.panel {
transition: transform 200ms ease, opacity 200ms ease;
}
.panel-enter {
transform: translateY(8px);
opacity: 0;
}9.4 虚拟列表
当列表数量很大时,不应一次渲染所有 DOM。
适合虚拟列表:
- 表格。
- 日志。
- 消息列表。
- 商品列表。
- 大型下拉选择器。
核心思想:只渲染视口附近的元素。
9.5 CSS containment
css
.expensive-widget {
contain: layout paint;
}contain 可以告诉浏览器某个区域的布局和绘制影响不会扩散到外部,有助于复杂页面隔离渲染成本。
10. 框架层性能优化
10.1 React
React 性能常见瓶颈:
- 组件重复渲染。
- 状态放得过高。
- context 更新范围过大。
- 列表 key 不稳定。
- 大组件同步计算。
- 未拆分路由 chunk。
优化方法:
- 合理拆分状态。
- 使用
memo、useMemo、useCallback,但不要滥用。 - 使用 React Profiler 找热点。
- 大列表使用虚拟滚动。
- 使用并发特性降低交互阻塞。
- 把服务端可完成的工作放到服务端。
错误倾向:看到重渲染就加 memo。正确做法是先用 Profiler 确认瓶颈。
编译期自动 memo:React Compiler
React Compiler 已于 2025 年 10 月发布 1.0 稳定版(react.dev 官方博客),并在 Meta 生产环境长期运行。它作为构建期 Babel 插件(内部将 AST 降为自有 HIR、做数据流与可变性分析),对组件和 hook 自动施加细粒度 memoization,甚至能做手动 memo 难以实现的"条件式记忆化"。对架构师的影响:
- 新项目可以把"手写
useMemo/useCallback/memo"降级为例外手段,官方推荐让编译器接管,只有编译器决策不符合预期时才手写。 - 它依赖 Rules of Hooks 的可分析性,因此 compiler-powered lint 规则已并入
eslint-plugin-react-hooks的 recommended preset——升级 hooks 插件、保证 Rules of Hooks 零违规,是启用编译器的前置治理项。 - 存量代码不要一次性删除手写 memo:官方指南明确删除可能改变编译输出,需灰度 + Profiler 验证。
⚠️ React Compiler 优化的是渲染阶段的计算(re-render 成本),不解决 bundle 体积、长任务和网络问题——它无法替代上面所有加载期优化。
10.2 Vue
Vue 性能关注:
- 响应式数据范围。
- 大对象深层响应式成本。
- 列表 key。
- computed 缓存。
- watch 过度使用。
- keep-alive 使用边界。
- 异步组件和路由懒加载。
优化方法:
- 使用
computed表达派生状态。 - 避免大对象不必要深度响应式。
- 使用
shallowRef/markRaw处理特殊对象。 - 大列表虚拟滚动。
- 路由级懒加载。
10.3 状态管理
状态管理性能问题通常来自:
- 全局 store 太大。
- 单次更新影响过多组件。
- selector 不稳定。
- 派生数据每次重算。
- 缓存策略缺失。
建议:
- 全局状态只放跨页面共享状态。
- 页面局部状态留在局部。
- 服务端数据交给 TanStack Query、SWR、Apollo 等专用工具。
- 对 expensive selector 做 memo。
10.4 数据请求
优化请求:
- 避免瀑布请求。
- 并行加载可并行数据。
- 缓存稳定数据。
- 请求去重。
- 预取下一步数据。
- 分页或增量加载。
- SSR 中避免重复请求。
11. 构建产物优化
11.1 Bundle 分析
工具:
rollup-plugin-visualizerwebpack-bundle-analyzersource-map-explorer- Vite 构建日志
分析重点:
- 最大依赖是谁。
- 是否重复打包。
- 是否引入了全量库。
- 是否存在多版本依赖。
- 首屏 chunk 是否过大。
11.2 Tree Shaking
Tree shaking 依赖:
- ESM 静态导入导出。
- 包的
sideEffects声明。 - 构建工具的优化能力。
- 代码本身没有隐式副作用。
避免:
js
import _ from "lodash";更好:
js
import debounce from "lodash/debounce";或选择原生 API / 更小工具库。
11.3 Code Splitting
常见拆分:
- 路由拆分。
- 管理后台和前台拆分。
- 图表库异步加载。
- 富文本编辑器异步加载。
- 国际化语言包按需加载。
不要过度拆分。过多小 chunk 会增加调度和请求管理成本。
11.4 依赖治理
定期检查:
- 是否有重复依赖版本。
- 是否引入了过大库。
- 是否有未使用依赖。
- 是否有 CJS 包影响 tree shaking。
- 是否有 polyfill 全量引入。
架构师应把 bundle 分析纳入发布流程,而不是线上慢了才排查。
12. 缓存策略
12.1 HTTP 缓存
静态资源:
http
Cache-Control: public, max-age=31536000, immutable前提:文件名带 hash,例如:
text
app.8f3a1c2.jsHTML:
http
Cache-Control: no-cache或短缓存加协商缓存。
12.2 Service Worker
Service Worker 可以实现:
- 离线访问。
- 静态资源缓存。
- API 缓存。
- 后台同步。
- 推送通知。
但它也会增加复杂度:
- 缓存版本管理。
- 更新策略。
- 回滚策略。
- 脏缓存问题。
不要在没有明确策略的情况下随意加入 Service Worker。
12.3 数据缓存
前端数据缓存要区分:
- 静态配置。
- 用户信息。
- 列表数据。
- 详情数据。
- 权限数据。
- 实时数据。
常用策略:
- stale-while-revalidate。
- 请求去重。
- 乐观更新。
- 后台刷新。
- 过期时间。
- 手动失效。
13. 运行时内存与长任务优化
13.1 内存泄漏来源
常见来源:
- 事件监听未移除。
- 定时器未清理。
- 闭包引用大对象。
- 缓存无限增长。
- DOM 节点被 JS 引用。
- WebSocket / Worker 未关闭。
- 第三方库持有引用。
13.2 排查内存泄漏
Chrome DevTools:
- Memory 面板。
- Heap snapshot。
- Allocation instrumentation。
- Detached DOM tree。
流程:
- 打开页面。
- 拍一次快照。
- 重复进入和离开页面。
- 再拍快照。
- 对比对象数量和 Retainers。
工具选择与操作细节:
| 工具 | 回答的问题 | 适用场景 |
|---|---|---|
| Heap snapshot | 某一时刻堆里有什么、被谁引用(Retainers 链) | 怀疑泄漏后定位持有者 |
| Allocation instrumentation on timeline | 分配随时间发生在哪、哪些没被回收 | 反复操作导致的缓慢泄漏 |
| Detached elements(Performance/Memory) | 已离开文档树但仍被 JS 引用的 DOM 节点 | 组件卸载后 DOM 泄漏 |
堆快照对比法(对比基准选 Objects between snapshot 1 and 2,且两次快照前都手动点一次垃圾回收图标):
- 冷启动页面,拍快照 A。
- 执行可疑操作 N 次(进出某路由、打开关闭某弹窗)。
- 手动 GC 后拍快照 B,选与 A 对比。
- 按 Retained Size 排序,看数量随 N 线性增长的构造器(如
Detached HTMLDivElement、闭包system / Object)。 - 点开 Retainers 链找到最终挂在
Window/ 全局 store / 事件监听器上的引用路径。
⚠️ 常见假阳性:Window 上的缓存(Map、全局数组)、DevTools 自身展开的对象、尚未过期的 debounce 定时器,都会让"增长"看起来像泄漏。判定标准是"重复同样操作是否单调增长、GC 后仍不释放",而不是"某次快照很大"。
💡 SPA 路由级泄漏的高频根因:useEffect/onUnmounted 里没清理 addEventListener(尤其 window/document 上的)、setInterval 没 clear、WebSocket/EventSource 没 close、观察器(IntersectionObserver/ResizeObserver)没 disconnect、把大对象写进了全局 store 或模块级缓存。React 严格模式下 effect 双执行也常暴露这类清理缺失。
13.3 长任务治理
长任务优化:
- 拆任务。
- 延后非关键任务。
- 使用 Worker。
- 减少大型同步 JSON 处理。
- 降低初始化阶段一次性工作量。
- 交互后先给视觉反馈,再做重活。
13.4 交互优先
好的交互性能不是所有任务都最快,而是用户最关心的反馈优先。
点击按钮后可以:
- 立即改变按钮状态。
- 再发请求。
- 再处理复杂数据。
- 最后更新非关键区域。
这比点击后先同步计算 500ms 更符合用户感受。
14. SSR、SSG、CSR 与性能取舍
14.1 CSR
CSR:客户端渲染。
优点:
- 前后端分离简单。
- 交互复杂应用开发体验好。
- 部署静态资源方便。
缺点:
- 首屏依赖 JS 下载和执行。
- SEO 需要额外处理。
- 弱网低端设备首屏可能慢。
14.2 SSR
SSR:服务端渲染。
优点:
- 首屏 HTML 更快可见。
- SEO 友好。
- 适合内容页、电商、营销页。
缺点:
- 服务端成本。
- hydration 成本。
- 缓存和数据一致性复杂。
- 错误边界更复杂。
14.3 SSG
SSG:静态站点生成。
优点:
- 访问速度快。
- CDN 缓存友好。
- 稳定可靠。
缺点:
- 内容更新需要重新生成。
- 个性化能力弱。
- 大规模动态页面构建时间长。
14.4 ISR / Edge Rendering
现代框架还提供:
- ISR:增量静态再生成。
- Edge Rendering:边缘渲染。
- Streaming SSR:流式渲染。
- Partial Hydration / Islands:局部水合。
选择策略要看业务:
- 后台系统:CSR 通常足够。
- 内容站:SSG / SSR。
- 电商详情:SSR / ISR。
- 个性化推荐:SSR + 缓存 + 客户端补充。
- 全球用户:CDN + Edge。
15. 性能监控与告警体系
15.1 为什么需要 RUM
本地和 CI 跑分不能代表全部用户。
真实用户受影响因素:
- 地区。
- 网络。
- 设备。
- 浏览器。
- 页面路径。
- 登录状态。
- AB 实验。
- 第三方服务。
RUM:Real User Monitoring,用真实用户数据衡量性能。
15.2 Web Vitals 上报
可以使用 web-vitals:
ts
import { onCLS, onINP, onLCP } from "web-vitals";
function sendToAnalytics(metric) {
navigator.sendBeacon("/analytics/web-vitals", JSON.stringify(metric));
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);上报时要记录:
- 页面路径。
- 指标名称。
- 指标值。
- 设备类型。
- 网络类型。
- 浏览器。
- 版本号。
- release id。
- 用户分群。
15.3 监控维度
建议看:
- P50 / P75 / P90 / P95。
- 按页面聚合。
- 按设备聚合。
- 按地区聚合。
- 按版本聚合。
- 按入口来源聚合。
只看平均值会掩盖长尾问题。
15.4 告警
告警不要太敏感,否则团队会疲劳。
适合告警:
- 某页面 P75 LCP 连续超过阈值。
- 新版本 INP 明显恶化。
- JS 错误率和性能退化同时出现。
- 某地区性能突降。
- 第三方脚本耗时突增。
16. 性能预算与团队治理
16.1 性能预算是什么
性能预算是团队提前定义的上限,例如:
- 首屏 JS gzip 后不超过 180KB。
- 单个路由 chunk 不超过 120KB。
- LCP P75 小于 2.5s。
- INP P75 小于 200ms。
- CLS P75 小于 0.1。
- 第三方脚本数量不超过 5 个。
16.2 为什么需要预算
没有预算,性能会缓慢退化:
- 加一个图表库。
- 加一个埋点 SDK。
- 加一个富文本编辑器。
- 加几个全量 icon。
- 加一个 polyfill。
每次改动都“不大”,半年后首屏就会很重。
16.3 CI 中做性能门禁
可以检查:
- bundle size。
- chunk size。
- Lighthouse CI。
- 关键路由性能。
- 未使用依赖。
- 重复依赖版本。
门禁不一定一开始就阻塞合并,可以先记录趋势,再逐步变严格。
16.4 团队制度
建议:
- 重要页面有性能 owner。
- 新增大型依赖需要说明理由。
- 首屏资源增长需要评审。
- 每个版本对比性能变化。
- 重大活动前做弱网和低端机测试。
- 性能优化结果要可回归验证。
17. 常见性能问题排查清单
17.1 LCP 慢
检查:
- TTFB 是否高。
- LCP 元素是什么。
- LCP 图片是否过大。
- LCP 图片是否懒加载。
- 是否缺少 preload / fetchpriority。
- CSS / JS 是否阻塞渲染。
- 是否 CSR 导致内容太晚出现。
- CDN 是否命中。
17.2 INP 差
检查:
- 主线程是否有长任务。
- 点击后是否同步计算大量数据。
- 是否一次性渲染大量 DOM。
- 是否有复杂表格或图表。
- 是否有第三方脚本抢占主线程。
- 框架是否重复渲染。
- 是否可以拆任务或用 Worker。
17.3 CLS 高
检查:
- 图片是否设置宽高。
- 广告位是否保留空间。
- 字体是否导致跳动。
- 异步内容是否插入到上方。
- 骨架屏尺寸是否一致。
- 页面初始化时是否频繁改变布局。
17.4 JS 包过大
检查:
- 是否引入全量库。
- 是否存在重复依赖。
- 是否包含 moment 全量 locale。
- 是否把图表、编辑器放进首屏。
- 是否 CJS 影响 tree shaking。
- 是否 polyfill 全量注入。
17.5 滚动卡顿
检查:
- 滚动监听是否过重。
- 是否缺少节流。
- 是否滚动时触发布局。
- 列表是否过大。
- 图片是否边滚边解码。
- CSS 阴影、滤镜是否过重。
18. 学习路线与练习
18.1 基础阶段
目标:理解指标与工具。
练习:
- 用 Lighthouse 跑一个页面。
- 找出 LCP 元素。
- 用 Performance 面板录制加载过程。
- 观察 Network 瀑布流。
- 写出页面当前最大瓶颈。
18.2 进阶阶段
目标:掌握常见优化手段。
练习:
- 优化一张首屏大图为 AVIF / WebP。
- 给图片设置宽高和响应式尺寸。
- 拆分一个非首屏路由 chunk。
- 延后加载第三方脚本。
- 对比优化前后 LCP。
18.3 高级阶段
目标:定位运行时卡顿。
练习:
- 制造一个 300ms 长任务。
- 用 Performance 面板定位。
- 把任务拆分为多个小任务。
- 使用 Web Worker 处理大量计算。
- 对比 INP 或交互延迟。
18.4 架构师阶段
目标:建立性能治理体系。
练习:
- 为项目定义性能预算。
- 接入 Web Vitals 上报。
- 按页面统计 P75。
- 在 CI 中加入 bundle size 检查。
- 写一份性能退化排查流程。
19. 官方参考资料
- Web Vitals:https://web.dev/articles/vitals
- Largest Contentful Paint:https://web.dev/articles/lcp
- Interaction to Next Paint:https://web.dev/articles/inp
- Cumulative Layout Shift:https://web.dev/articles/cls
- Optimize LCP:https://web.dev/articles/optimize-lcp
- Optimize INP:https://web.dev/articles/optimize-inp
- Optimize CLS:https://web.dev/articles/optimize-cls
- Chrome DevTools Performance:https://developer.chrome.com/docs/devtools/performance
- PageSpeed Insights:https://developers.google.com/speed/docs/insights/v5/about
- Chrome UX Report:https://developer.chrome.com/docs/crux
- MDN Web performance:https://developer.mozilla.org/en-US/docs/Web/Performance
- web-vitals:https://github.com/GoogleChrome/web-vitals
- Long Animation Frames API:https://developer.chrome.com/docs/web-platform/long-animation-frames
- Use scheduler.yield() to break up long tasks:https://developer.chrome.com/blog/use-scheduler-yield
- INP becomes a Core Web Vital(FID 弃用公告):https://web.dev/blog/inp-cwv-march-12