性能优化不是零散技巧的堆砌,而是一套"测量---定位---优化---验证---持续监控"的工程体系。本文沿着浏览器从接收 URL 到完成页面呈现的全过程,系统梳理构建、资源、缓存、存储、CDN、渲染、DOM、事件调度、懒加载和性能监测,并对部分历史方案给出当前更合适的替代方式。
一、先建立完整的性能优化地图
用户输入 URL 后,页面大致会经历以下过程:
- DNS 查询并建立网络连接;
- 浏览器发起 HTTP 请求,服务端处理并返回资源;
- 浏览器下载 HTML、CSS、JavaScript、图片和字体;
- HTML 与 CSS 分别被解析为 DOM 和 CSSOM;
- 浏览器计算样式、布局、绘制并合成页面;
- JavaScript 执行业务逻辑,页面进入可交互状态;
- 用户继续滚动、点击、输入,应用持续更新界面。
因此,前端性能至少包含三条主线:
- 网络效率:请求是否足够少、足够小、足够近,缓存是否有效;
- 渲染效率:主线程是否繁忙,DOM、样式、布局和绘制是否合理;
- 交互体验:内容是否快速出现,交互是否及时响应,页面是否稳定。
真正有效的优化必须从指标和瓶颈出发,而不是看到某项技术就机械套用。建议先设立性能预算,例如:首屏 JavaScript 体积、图片体积、接口响应时间、Core Web Vitals 和低端设备上的长任务数量,再让每次优化都有明确目标。
二、构建优化与 HTTP 压缩
构建阶段主要解决两个问题:开发构建太慢 和生产产物太大。
2.1 提升构建速度
无论使用 Webpack、Vite 还是其他工具,都可以从以下方向入手:
- 缩小转译范围,只处理真正需要处理的源码目录;
- 启用持久化缓存和增量构建,避免重复计算;
- 减少不必要的 Loader、插件和重复的代码转换;
- 优先选择基于 esbuild、SWC、Oxc、Rolldown 等原生工具链的转换能力;
- 谨慎开启多进程。并行处理存在进程启动和通信成本,只有单任务足够重时才有收益;
- 定期升级 Node.js、包管理器和构建工具,及时获得解析与缓存优化;
- 对构建过程做性能分析,不凭感觉增加配置。
Webpack 官方至今仍建议缩小 Loader 作用范围、使用持久化缓存,并控制并行任务的额外开销;Vite 则通过依赖预构建、按需转换和模块级热更新提升开发体验。
2.2 控制生产包体积
- 使用 ESM,保证 Tree Shaking 能识别未使用导出;
- 用动态
import()按路由或业务模块拆包; - 将稳定的公共依赖与业务代码合理分离,配合内容哈希获得长期缓存;
- 使用 Bundle Analyzer 检查重复依赖、大型库和意外打包的资源;
- 删除无用代码、语言包、Polyfill 和重复组件;
- 对首屏关键代码设置体积预算,在 CI 中阻止明显回退。
js
// 现代按需加载写法
const ReportPage = () => import('./pages/ReportPage.js')
课程中出现的一些方案带有明显时代背景,理解其目标即可,不建议在新项目中直接照搬:
| 历史方案 | 当时解决的问题 | 当前更常见的做法 |
|---|---|---|
DllPlugin |
稳定依赖重复编译 | 持久化缓存、依赖预构建、合理拆包 |
HappyPack |
Loader 并行执行 | 原生高速编译器、按需使用线程池 |
require.ensure |
代码分割 | 标准动态 import() |
UglifyJsPlugin |
JavaScript 压缩 | Terser、esbuild、SWC、Oxc 等 |
2.3 Gzip、Brotli 与传输压缩
浏览器会在请求头中通过 Accept-Encoding 告知服务器支持的压缩算法,服务端压缩响应后用 Content-Encoding 标识实际算法。文本类资源通常非常适合压缩:
- HTML、CSS、JavaScript、JSON、SVG 可使用 Gzip 或 Brotli;
- JPEG、WebP、AVIF、视频等已经压缩的资源通常不值得再次压缩;
- 极小文件的压缩收益可能小于头部和计算成本;
- 静态资源可以提前生成
.gz、.br文件,减少运行时 CPU 消耗; - CDN 或网关应正确区分压缩版本,并设置
Vary: Accept-Encoding。
压缩的核心是减少传输字节,但压缩等级越高,CPU 成本通常也越高。应以真实文件和真实流量测试结果为准。
三、图片优化:在清晰度与体积之间做选择
图片经常是页面中最大的资源类型。优化不能只看格式,还要同时考虑内容特征、展示尺寸、设备像素比和加载时机。
| 格式 | 适合场景 | 主要特点 |
|---|---|---|
| JPEG | 照片、复杂背景、宣传图 | 有损压缩,不支持透明 |
| PNG | 需要无损和透明的图形 | 细节清晰,但照片类内容通常较大 |
| SVG | 图标、Logo、简单插画、图表 | 矢量、可缩放、可编程,复杂 SVG 也会增加解析与渲染成本 |
| WebP | 通用图片与动图 | 支持有损、无损、透明和动画 |
| AVIF | 对体积要求较高的照片类资源 | 通常具有较高压缩效率,但编码成本和兼容回退仍需考虑 |
3.1 现代图片交付方式
使用 <picture> 提供 AVIF、WebP 和传统格式回退,并通过 srcset、sizes 让浏览器选择接近实际展示尺寸的文件:
html
<picture>
<source
type="image/avif"
srcset="/hero-640.avif 640w, /hero-1280.avif 1280w"
/>
<source
type="image/webp"
srcset="/hero-640.webp 640w, /hero-1280.webp 1280w"
/>
<img
src="/hero-1280.jpg"
srcset="/hero-640.jpg 640w, /hero-1280.jpg 1280w"
sizes="(max-width: 768px) 100vw, 1200px"
width="1280"
height="720"
alt="产品首页概览"
/>
</picture>
3.2 Base64 与雪碧图的取舍
Base64 不是图片格式,而是把二进制编码进文本。它可以减少一次独立请求,却会增大编码体积、占用主文档缓存,并失去图片独立更新与缓存的能力。因此只适合极小、稳定、首屏必需的资源。
雪碧图曾通过合并小图减少 HTTP/1.1 下的请求数量,但在 HTTP/2、HTTP/3 和 SVG 图标体系中,其收益已经降低,维护成本却仍然存在。新项目通常优先选择 SVG Sprite、图标组件或独立的小型缓存资源。
3.3 图片优化清单
- 上传阶段进行尺寸裁剪、质量压缩和格式转换;
- 不向 300px 的容器发送 3000px 的原图;
- 为图片声明
width、height或aspect-ratio,避免布局偏移; - 非首屏图片懒加载,首屏 LCP 图片不要懒加载;
- 关键图片可使用
fetchpriority="high",但不要滥用; - 使用图片 CDN 按设备动态裁剪和转换格式;
- 图片失败时提供合理占位和降级方案。
四、浏览器缓存:让资源不必重复下载
浏览器缓存不止一种。内存缓存速度快但生命周期短;HTTP 缓存依据响应头复用网络资源;Service Worker 与 Cache API 可以实现离线和自定义缓存策略。
4.1 强缓存
强缓存命中时,浏览器可直接复用资源,无须向源站验证。
Cache-Control: max-age=31536000:资源在指定秒数内保持新鲜;immutable:资源新鲜期内不会变化,避免无意义验证;s-maxage:控制 CDN、代理等共享缓存;public与private:决定响应能否被共享缓存保存。
Expires 使用绝对时间,容易受到客户端时钟影响;现代项目通常以 Cache-Control 为主。
4.2 协商缓存
缓存过期后,浏览器可向服务器询问资源是否变化:
Last-Modified/If-Modified-Since以修改时间判断;ETag/If-None-Match以资源标识判断;- 未变化时服务端返回
304 Not Modified,避免传输完整响应体。
ETag 通常更精确,但生成和比较也需要成本。二者可以按资源类型与服务端能力组合使用。
4.3 最容易混淆的指令
| 指令 | 正确含义 |
|---|---|
no-cache |
可以存储,但每次复用前必须验证 |
no-store |
不应存储该响应 |
max-age=0 |
立即过期,通常仍会进入验证流程 |
private |
只允许用户私有缓存保存,不允许 CDN 等共享缓存保存 |
推荐配置:
http
# 带内容哈希的静态资源
Cache-Control: public, max-age=31536000, immutable
# 需要及时获取新版本的 HTML
Cache-Control: no-cache
# 高敏感、确实不应落盘的数据
Cache-Control: no-store
静态资源必须配合内容哈希,例如 app.a8f31c.js。文件内容变化时 URL 也变化,才能安全地使用一年强缓存。
课程提到的 Push Cache 属于 HTTP/2 Server Push 的相关机制。Chrome 已默认关闭 Server Push,新项目应优先考虑普通缓存、preload、preconnect 或服务端 103 Early Hints,且任何提前加载策略都需要用数据验证。
五、本地存储:从 Cookie 到 IndexedDB
本地存储的选择取决于数据是否需要随请求发送、数据规模、结构复杂度、生命周期和安全级别。
| 方案 | 数据形态 | 访问方式 | 典型用途 |
|---|---|---|---|
| Cookie | 小型字符串 | 随匹配请求发送 | 会话标识、服务端状态 |
localStorage |
字符串键值 | 同步 | 少量低频配置、主题、简单草稿 |
sessionStorage |
字符串键值 | 同步 | 当前标签页的临时状态 |
| IndexedDB | 结构化数据、Blob | 异步 | 大量离线数据、索引查询、复杂草稿 |
| Cache API | 请求与响应对象 | 异步 | Service Worker 离线资源和网络策略 |
5.1 Cookie 不是通用存储
Cookie 会随着匹配域名和路径的请求发送,存放大量业务数据会增加网络负担。用于身份会话时应合理配置:
Secure:只通过 HTTPS 发送;HttpOnly:阻止 JavaScript 读取;SameSite:降低跨站请求风险;- 尽量缩小
Domain与Path范围; - 设置合适的过期时间。
5.2 Web Storage 的同步成本
localStorage 与 sessionStorage 使用简单,但 API 同步执行。大对象频繁序列化、反序列化会阻塞主线程,不适合高频写入和大量数据。不要因为"本地可用"就把访问令牌或敏感信息随意放进去;一旦页面发生 XSS,JavaScript 可读取的数据就可能泄露。
5.3 IndexedDB 与存储配额
IndexedDB 是异步结构化数据库,支持事务、索引和 Blob,适合离线应用与复杂本地数据。其容量并非"无限",不同浏览器会根据磁盘、来源和用户行为实施配额及回收策略。应用应处理配额异常,并把浏览器存储视为可丢失数据:重要数据仍需与服务端同步。
六、CDN:缓存、回源与边缘交付
CDN 将资源缓存到离用户更近的边缘节点,主要收益是缩短网络距离、降低源站压力并提高并发承载能力。
一次典型请求包含两种情况:
- 缓存命中:边缘节点直接返回内容;
- 缓存未命中:边缘节点从源站拉取资源,也就是"回源",随后按策略缓存。
CDN 优化的关键不是"接入即可",而是提高有效命中率并控制错误缓存:
- 静态资源使用内容哈希和长缓存;
- HTML、接口和个性化内容分别设计缓存策略;
- 缓存 Key 应正确处理查询参数、设备、语言和压缩格式;
- 避免把含用户隐私的响应缓存为公共内容;
- 发布时可预热热点资源,异常时支持刷新和版本回滚;
- 监控命中率、回源带宽、边缘错误率和区域延迟;
- 使用源站保护、分层缓存和
stale-while-revalidate降低回源洪峰。
独立静态资源域名可以避免携带无关 Cookie,但额外域名也会增加 DNS、连接与 TLS 成本。HTTP/2、HTTP/3 下不要为了"多并发"过度拆分域名。
七、服务端渲染:优化首屏与内容可发现性
客户端渲染(CSR)通常先返回页面外壳,再下载 JavaScript、请求数据并生成内容;服务端渲染(SSR)则在服务端生成当前请求对应的 HTML,使用户更早看到主体内容。
SSR 的优势包括:
- 更稳定的首屏内容呈现;
- 对弱设备更友好,首次内容不完全依赖客户端计算;
- 便于搜索引擎和分享抓取获取完整内容。
SSR 也有成本:
- 服务端计算和运维复杂度提高;
- 服务端响应时间可能变长;
- HTML 出现不等于页面已经可交互,客户端仍需 Hydration;
- 服务端和客户端环境差异会带来一致性问题。
现代项目应按页面选择渲染模式:
| 页面类型 | 优先考虑 |
|---|---|
| 营销页、文档、博客 | SSG 或增量静态生成 |
| 新闻、商品详情等动态内容 | SSR、流式 SSR 或混合渲染 |
| 登录后的复杂后台 | CSR,必要时只为壳层或关键路由 SSR |
| 高度个性化且实时的页面 | SSR 与客户端数据更新结合 |
"所有页面都 SSR"不是目标。更合理的做法是按路由、数据新鲜度和交互复杂度组合 CSR、SSR、SSG、边缘缓存与流式输出。
八、浏览器渲染机制:理解页面为什么会慢
页面渲染可以概括为以下流水线:
text
HTML → DOM ┐
├→ 样式计算 → 渲染树 → 布局 → 绘制 → 合成
CSS → CSSOM┘
- HTML 解析生成 DOM;
- CSS 解析生成 CSSOM;
- 浏览器结合两者计算每个节点的样式;
- Layout 确定元素尺寸与位置;
- Paint 生成绘制记录;
- Composite 将各图层组合到屏幕。
JavaScript 可以读取和修改 DOM、CSSOM,因此普通脚本可能阻塞 HTML 解析。脚本加载方式应按依赖关系选择:
| 方式 | 下载 | 执行时机 | 顺序 |
|---|---|---|---|
普通 <script> |
阻塞解析 | 下载后立即执行 | 文档顺序 |
async |
并行下载 | 下载完成立即执行 | 不保证 |
defer |
并行下载 | HTML 解析完成后执行 | 保持文档顺序 |
type="module" |
并行下载 | 默认类似 defer |
按模块依赖执行 |
优化重点包括:
- 关键 CSS 尽早到达,非关键样式避免阻塞首屏;
- 非首屏脚本延后或按需加载;
- 只为明确的关键资源使用
preload; - 对确定会访问的跨域源使用
preconnect,但控制数量; - 减少 DOM 规模和频繁的样式失效;
- CSS 选择器保持清晰、作用域明确、层级不过深。
现代浏览器已经对选择器匹配做了大量优化。相较于纠结单个选择器的纳秒级差异,减少无效 DOM、样式重算和布局通常更有价值。
九、DOM 优化:减少跨层操作和无效更新
JavaScript 引擎操作 DOM 时,需要与浏览器渲染模块协作;修改结果还可能触发样式计算、布局和绘制。因此,优化 DOM 的核心不是"永远不用 DOM",而是减少操作次数、合并更新、降低节点规模。
9.1 批量创建与一次提交
js
const list = document.querySelector('#list')
const fragment = document.createDocumentFragment()
for (const item of data) {
const li = document.createElement('li')
li.textContent = item.name
fragment.appendChild(li)
}
list.appendChild(fragment)
DocumentFragment 允许先在脱离主文档的容器中组织节点,最后一次性插入。若使用 innerHTML 批量生成内容,必须确认数据可信或完成安全转义,避免 XSS。
9.2 常见实践
- 缓存重复使用的 DOM 引用,不在循环中反复查询;
- 将多次内容修改合并成一次提交;
- 用事件委托减少大量相似监听器;
- 长列表使用分页、虚拟列表或窗口化渲染;
- 不渲染不可见且暂时无用的复杂组件;
- 在框架中保持稳定的
key,减少无效节点替换; - 通过性能工具验证,避免为很小的列表过度设计。
虚拟 DOM 和响应式框架可以降低直接操作 DOM 的复杂度,但并不会消除真实渲染成本。组件层级、更新范围和列表规模仍然需要控制。
十、Event Loop 与异步更新
浏览器主线程同时负责 JavaScript、事件分发、样式计算、布局和绘制。JavaScript 采用运行至完成模型:一个任务未执行结束,主线程就无法处理下一次交互或渲染。
10.1 任务、微任务与渲染机会
- 脚本执行、定时器和事件回调通常进入任务队列;
- Promise 回调、
queueMicrotask()和 MutationObserver 使用微任务队列; - 一个任务结束后,浏览器会清空当前微任务队列,然后在合适时机进行渲染。
微任务更早执行,但并不意味着"所有 DOM 更新都应该塞进微任务"。微任务会一直执行到队列清空,递归添加微任务可能长期占用主线程,让渲染和交互得不到机会。
10.2 框架为什么批量更新
如果连续三次修改同一个状态,而界面只需要最终结果,同步执行三次 DOM 更新显然浪费。Vue、React 等框架会将同一批状态变化合并,在调度点集中计算和提交,从而减少无效渲染。
nextTick 的用途是等待框架完成当前批次的 DOM 更新后再执行代码,而不是提升所有异步逻辑的万能工具。使用它通常意味着后续逻辑确实依赖更新后的 DOM。
10.3 让主线程及时"喘气"
- 把超过约 50ms 的长任务拆成小任务;
- 大量纯计算放进 Web Worker;
- 动画更新使用
requestAnimationFrame(); - 非关键任务可以延后或在空闲阶段处理;
- 减少第三方脚本,并延迟非必要 SDK;
- 通过 Performance 火焰图定位真正阻塞主线程的函数。
十一、回流、重绘与合成
不同样式变化的成本并不相同:
- 改变尺寸、位置、字体等几何信息,通常会触发 Layout;
- 改变颜色、背景等外观,通常需要 Paint;
- 在适当图层上的
transform、opacity动画,往往可以主要交给 Composite。
11.1 强制同步布局
浏览器会尽量批处理样式变化,但如果代码先写入样式,又立即读取 offsetWidth、getBoundingClientRect()、getComputedStyle() 等布局信息,浏览器为了返回准确结果,可能被迫提前完成布局。
js
// 不理想:读写交错,循环中可能反复触发布局
for (const el of items) {
el.style.width = `${container.offsetWidth / 2}px`
}
// 更好:先统一读取,再统一写入
const targetWidth = container.offsetWidth / 2
requestAnimationFrame(() => {
for (const el of items) {
el.style.width = `${targetWidth}px`
}
})
11.2 降低渲染成本
- 把 DOM 读取和写入分组,避免交错;
- 用 class 切换一组样式,提升可维护性并便于批处理;
- 批量新增节点时使用 Fragment 或脱离文档的容器;
- 动画优先使用
transform、opacity与requestAnimationFrame(); - 使用
contain、content-visibility等能力限制复杂区域的影响范围; - 不要无节制使用
will-change,额外图层会占用内存; - 在 DevTools 中观察 Layout、Paint、图层和掉帧,而不是背诵属性表。
将节点暂时设为 display: none 再批量修改,是传统的"离线 DOM"手法,但隐藏和恢复本身也可能触发布局。现代项目应优先考虑 Fragment、批量提交和组件级离屏渲染,并用数据判断是否值得。
十二、懒加载:先把用户眼前的内容做好
懒加载的本质是推迟非关键资源:首屏只加载用户立即能看到的内容,接近视口时再加载下方图片、组件或数据。
12.1 图片优先使用原生懒加载
html
<img
src="/product.webp"
loading="lazy"
width="640"
height="480"
alt="商品展示"
/>
注意:
- 首屏主图或 LCP 图片不要设置
loading="lazy"; - 提前声明尺寸,避免图片出现时推动页面内容;
- 懒加载不能代替图片压缩和响应式尺寸;
- 加载距离由浏览器决定,业务不必默认监听滚动事件。
12.2 复杂场景使用 Intersection Observer
js
const observer = new IntersectionObserver((entries, obs) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
const img = entry.target
img.src = img.dataset.src
obs.unobserve(img)
}
}, { rootMargin: '200px 0px' })
document
.querySelectorAll('img[data-src]')
.forEach((img) => observer.observe(img))
rootMargin 可以让资源在真正进入视口前开始加载,减少用户看到空白占位的概率。只有在兼容性或特殊滚动容器限制下,才退回到 scroll、getBoundingClientRect() 和节流方案。
十三、节流与防抖
高频触发的 scroll、resize、pointermove、input 等事件可能产生大量计算。节流和防抖都用于控制频率,但语义不同。
| 策略 | 行为 | 适合场景 |
|---|---|---|
| 节流 throttle | 持续触发期间,每隔一段时间最多执行一次 | 滚动进度、拖拽、持续采样 |
| 防抖 debounce | 停止触发一段时间后执行最后一次 | 搜索建议、表单校验、窗口调整结束 |
js
function debounce(fn, delay = 300) {
let timer
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
function throttle(fn, interval = 100) {
let lastTime = 0
let timer
return function (...args) {
const now = Date.now()
const remaining = interval - (now - lastTime)
if (remaining <= 0) {
clearTimeout(timer)
timer = undefined
lastTime = now
fn.apply(this, args)
return
}
clearTimeout(timer)
timer = setTimeout(() => {
lastTime = Date.now()
timer = undefined
fn.apply(this, args)
}, remaining)
}
}
生产环境还要考虑首次是否立即执行、尾部是否补执行、是否支持 cancel()、组件销毁时如何清理,以及异步请求竞态。成熟工具库已经处理了这些边界,不必在每个项目重复造轮子。
节流与防抖并非越多越好。若浏览器已经提供更合适的 API,例如 Intersection Observer、Resize Observer 或 requestAnimationFrame(),应优先使用语义更准确的方案。
十四、性能监测:没有数据,就没有优化结论
性能监测可分为两类:
- 实验室数据:在固定设备与网络条件下重复测试,适合开发和回归;
- 真实用户数据(RUM):采集用户真实设备、网络和地域下的体验,适合发现长尾问题。
14.1 Chrome DevTools Performance
Performance 面板可以记录页面加载与交互过程。分析时重点观察:
- Network:关键资源的开始时间、优先级和瀑布关系;
- Main:主线程任务、函数调用和长任务;
- Timings:LCP、布局偏移和自定义标记;
- Frames:动画帧率与掉帧;
- Bottom-up / Call tree:耗时最终集中在哪些函数;
- Layout / Paint:是否存在频繁布局和大面积绘制。
测试前应尽量固定网络、CPU、缓存和浏览器扩展条件,否则两次结果不可直接比较。
14.2 Lighthouse
Lighthouse 已集成在 Chrome DevTools 中,也可以使用命令行和 CI。它适合快速发现阻塞资源、图片尺寸、缓存、主线程工作等问题,但单次分数会受到环境波动影响。正确用法是保存基线、重复测试,并结合具体审计项和真实用户数据,而不是只追求"100 分"。
14.3 Performance API
课程中的 window.performance.timing 已废弃,现代代码应使用 PerformanceNavigationTiming:
js
const [navigation] = performance.getEntriesByType('navigation')
if (navigation) {
console.table({
DNS: navigation.domainLookupEnd - navigation.domainLookupStart,
TCP: navigation.connectEnd - navigation.connectStart,
TTFB: navigation.responseStart - navigation.requestStart,
DOMReady:
navigation.domContentLoadedEventEnd - navigation.startTime,
Load: navigation.loadEventEnd - navigation.startTime,
})
}
业务阶段可使用 User Timing:
js
performance.mark('search-start')
await loadSearchResult()
performance.mark('search-end')
performance.measure('search', 'search-start', 'search-end')
14.4 Core Web Vitals 与真实用户上报
当前核心体验指标为:
- LCP:主要内容加载速度,良好值不高于 2.5 秒;
- INP:交互响应能力,良好值不高于 200 毫秒;
- CLS:视觉稳定性,良好值不高于 0.1。
评估站点时应关注移动端与桌面端各自第 75 百分位,而不是只看开发者电脑上的一次结果。可使用 web-vitals 库采集:
js
import { onCLS, onINP, onLCP } from 'web-vitals'
function report(metric) {
const body = JSON.stringify({
...metric,
page: location.pathname,
version: document.documentElement.dataset.appVersion ?? 'unknown',
})
const sent = navigator.sendBeacon?.('/performance', body)
if (!sent) {
fetch('/performance', {
method: 'POST',
body,
keepalive: true,
headers: { 'Content-Type': 'application/json' },
})
}
}
onCLS(report)
onINP(report)
onLCP(report)
上报时还应关联应用版本、页面、设备、网络、地域和用户会话,才能定位"哪个版本、哪类用户、哪条路径"发生了退化。
十五、把性能优化变成持续工程
课程的终点不应是一份技巧清单,而应是一张可继续扩展的知识索引。DNS 预解析、资源提示、Service Worker、Web Worker、PWA、边缘渲染、性能平台等专题,都可以沿着本文的全链路继续深入。
建议把性能工作固化为以下闭环:
- 建立基线:记录关键页面、核心设备和主要指标;
- 定位瓶颈:判断问题位于网络、服务端、资源还是主线程;
- 按收益排序:优先处理影响用户多、收益大、风险可控的问题;
- 小步实施:一次改动解决一个明确问题;
- 前后对比:使用相同条件验证指标是否改善;
- 灰度观察:用真实用户数据检查长尾和副作用;
- 防止回退:设置资源预算、Lighthouse CI 与监控告警;
- 沉淀规范:把有效经验写入组件、脚手架、发布流程和团队文档。
最终检查清单
网络与资源
- 首屏资源是否经过压缩、拆分和优先级设计?
- 图片是否使用合适格式、尺寸与加载时机?
- 静态资源是否使用内容哈希和长期缓存?
- CDN 命中率、回源量和缓存 Key 是否正常?
渲染与交互
- 是否存在阻塞首屏的脚本和样式?
- 是否存在超大 DOM、长列表或无效更新?
- 是否存在读写交错导致的强制同步布局?
- 长任务是否已经拆分或移入 Worker?
- 高频事件是否使用了更合适的浏览器 API 或频率控制?
监测与治理
- 是否同时具备实验室数据和真实用户数据?
- 是否按第 75 百分位观察 LCP、INP、CLS?
- 发布前后是否有相同条件的对比?
- CI 和线上监控能否发现性能回退?
结语
前端性能优化的本质,是管理用户等待:让关键内容更早到达,让主线程更快响应,让界面稳定地完成每一次变化。构建工具、图片格式、缓存策略和框架都会继续演进,但"先测量、再定位、后优化、再验证"的方法不会过时。
掌握原理之后,不必追逐所有技巧。找到当前产品最真实的瓶颈,做一项可验证的改进,再把它沉淀为工程能力,才是性能优化真正产生价值的方式。