前端性能优化实践指南
前端性能优化是一个老生常谈却又永远不过时的话题。这篇文章整理了我在实际项目中用到过的优化手段,从加载到渲染到运行时,尽量覆盖完整。
加载优化
资源压缩
所有文本资源都应该在部署前压缩。HTML、CSS、JavaScript 开启 gzip 或 brotli 压缩后通常能减小 60%-80% 的体积。
图片是最占带宽的资源。WebP 格式比 JPEG/PNG 小 25%-35%,AVIF 又比 WebP 小 20%。用 <picture> 标签做格式降级:
<picture>
<source srcset="photo.avif" type="image/avif" />
<source srcset="photo.webp" type="image/webp" />
<img src="photo.jpg" alt="" />
</picture>
代码分割
不要把所有 JS 打成一个包。用动态 import() 按路由或功能拆分:
// 路由级别懒加载
const Dashboard = () => import("./Dashboard.svelte");
// 功能级别懒加载
button.addEventListener("click", async () => {
const { openModal } = await import("./modal");
openModal();
});
预加载关键资源
对首屏必需的资源用 <link rel="preload"> 提前加载,对下一页可能用到的资源用 <link rel="prefetch"> 空闲时拉取:
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="prefetch" href="/next-page.js" />
渲染优化
减少 DOM 操作
频繁操作 DOM 是性能杀手。批量更新、使用 DocumentFragment、避免在循环中读布局属性:
// 差
items.forEach((item) => {
list.appendChild(createEl(item)); // 每次 append 都触发 reflow
});
// 好
const fragment = document.createDocumentFragment();
items.forEach((item) => {
fragment.appendChild(createEl(item));
});
list.appendChild(fragment); // 只触发一次 reflow
使用 CSS contain
contain 属性告诉浏览器某个元素及其子树与页面其他部分独立,可以单独优化布局和绘制:
.widget {
contain: layout style paint;
}
这对列表项、卡片组件特别有效。
避免强制同步布局
读写布局属性交替进行会强制浏览器同步计算样式。用 FastDOM 模式:先批量读、再批量写:
// 差:读写在循环中交替
elements.forEach((el) => {
const height = el.offsetHeight; // 读 → 强制布局
el.style.height = height * 2 + "px"; // 写
});
// 好:先读后写
const heights = elements.map((el) => el.offsetHeight); // 批量读
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + "px"; // 批量写
});
运行时优化
防抖与节流
高频事件(scroll、resize、input)必须做防抖或节流:
// 节流:固定间隔执行
function throttle(fn, ms) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= ms) {
last = now;
fn(...args);
}
};
}
// 防抖:停止后执行
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
Web Worker
耗时计算移到 Worker 线程,不阻塞主线程:
const worker = new Worker("sort-worker.js");
worker.postMessage(largeArray);
worker.onmessage = (e) => {
renderSorted(e.data);
};
虚拟列表
当列表项超过几百条时,不要全部渲染。只渲染视口可见的部分,上下各留少量缓冲项。这是长列表性能问题的标准解法。
核心思路:根据滚动位置计算可见范围,只渲染范围内的 DOM,用 padding 或 transform 模拟完整列表的高度。
网络优化
HTTP/2 多路复用
HTTP/2 下多个请求可以复用一个 TCP 连接,不再需要域名分片。反而,过多域名会增加 DNS 解析和连接开销。尽量用单一域名。
Service Worker 缓存
用 Service Worker 拦截请求,实现离线可用和缓存策略:
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
}),
);
});
缓存策略根据资源类型选择:HTML 用 network-first,JS/CSS 用 cache-first,图片用 stale-while-revalidate。
接口合并与按需
一个页面调十个接口比调一个合并接口慢得多。后端提供聚合接口,前端按需请求字段,不要一股脑全拿。
监控与度量
Core Web Vitals
Google 定义的核心指标:
- LCP(Largest Contentful Paint):最大内容渲染时间,目标 2.5s 内
- INP(Interaction to Next Paint):交互响应延迟,目标 200ms 内
- CLS(Cumulative Layout Shift):布局偏移,目标 0.1 内
性能预算
给关键指标设阈值,超出就报警。比如 JS 总量不超过 200KB(压缩后),首屏 LCP 不超过 2.5s。
持续监控
Lighthouse 只能看瞬间快照。真实用户监控(RUM)才能反映实际体验。用 PerformanceObserver 采集字段数据上报:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
reportMetric(entry.name, entry.startTime);
}
}).observe({ type: "largest-contentful-paint", buffered: true });
最后
性能优化没有银弹,每一步都是权衡。先度量,再优化,最后验证效果。盲目优化不如不优化。
保持简单,保持克制。