"我们的网站 LCP 是 5 秒怎么办?""老板说 PageSpeed Insights 分数太低" —— Web 性能优化是前端工程师的必修课,直接影响用户体验、转化率、SEO 排名。这篇文章把 Core Web Vitals 各个指标的优化手段讲透,涵盖从首屏加载到运行时交互的全部环节。
Core Web Vitals 三大指标
LCP(Largest Contentful Paint)
首屏最大内容元素渲染时间。目标 < 2.5s。常见 LCP 元素:首屏大图、首屏视频海报、大块文字。
INP(Interaction to Next Paint)
2024 起替代 FID。用户每次交互(点击 / 按键)到下一次渲染的延迟。目标 < 200ms。
CLS(Cumulative Layout Shift)
页面加载过程中累计的"意外位移"。目标 < 0.1。常见原因:图片没设宽高、字体切换、广告加载后插入。
LCP 优化
1. 关键资源 preload
<link rel="preload" href="/hero.webp" as="image">
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
2. 图片优化
<img src="hero.jpg" loading="eager" fetchpriority="high"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
sizes="(max-width: 768px) 100vw, 50vw"
width="800" height="600"
alt="...">
关键点:首屏图 fetchpriority="high",响应式 srcset,显式 width/height(避免 CLS)。
3. CSS 阻塞
关键 CSS 内联到 <head>,非关键 CSS 延迟。Critical / Critters 工具可以自动提取。
4. JS 阻塞
<!-- 默认 script 阻塞 HTML 解析 -->
<script src="app.js"></script>
<!-- defer:下载并行,DOM 解析完再执行 -->
<script src="app.js" defer></script>
<!-- async:下载并行,下载完立刻执行(可能打断解析) -->
<script src="analytics.js" async></script>
<!-- module 默认 defer -->
<script type="module" src="app.js"></script>
5. CDN + HTTP/2/3
静态资源走 CDN,边缘节点减少 RTT。HTTP/2 多路复用避免连接数瓶颈。HTTP/3 + QUIC 进一步降低延迟。
INP 优化
1. 减少长任务
主线程任务 > 50ms 都是"长任务"。优化:
// 长任务:在一个 tick 里干完所有事
function processItems(items) {
items.forEach(item => expensive(item));
}
// 分片:让出主线程
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
expensive(items[i]);
if (i % 50 === 0) await new Promise(r => setTimeout(r, 0));
}
}
// 用 scheduler.yield(2024 起新 API)
async function process() {
for (const item of items) {
expensive(item);
if (navigator.scheduling?.isInputPending()) {
await scheduler.yield();
}
}
}
2. Web Worker 后台计算
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ data: bigArray });
worker.onmessage = (e) => render(e.data);
// worker.js
onmessage = (e) => {
const result = heavyCompute(e.data);
postMessage(result);
};
3. requestIdleCallback
低优先级任务(日志上报、预渲染)等浏览器空闲时再做。
4. 输入响应优化
// 防抖:用户停止输入 300ms 才搜索
input.addEventListener('input', debounce(search, 300));
// 节流:滚动事件最多 100ms 一次
window.addEventListener('scroll', throttle(handleScroll, 100));
CLS 优化
1. 给图片视频设尺寸
<img src="..." width="800" height="600">
<!-- 浏览器加载前就知道占多大空间,不会插入后跳动 -->
# CSS aspect-ratio 也行
img { aspect-ratio: 4 / 3; width: 100%; height: auto; }
2. 字体优化
@font-face {
font-family: 'Custom';
src: url('font.woff2') format('woff2');
font-display: swap; /* 先显示系统字体,加载完再切 - 减少 FOIT 但有 FOUT */
font-display: optional; /* 加载快用,否则永远不用 - 更严格 */
}
# 预加载关键字体
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
3. 占位符
# 广告 / 嵌入内容预留固定高度
.ad-slot { min-height: 250px; }
# 图片骨架屏
.img-skeleton { background: #f0f0f0; aspect-ratio: 16/9; }
资源加载优化
1. Tree Shaking
用 ES Module 导入,打包工具(Webpack / Rollup / esbuild)能删除未用代码。
2. Code Splitting
// 路由级分割
const Settings = lazy(() => import('./Settings'));
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>
3. 压缩
JS / CSS / HTML / SVG 都用 gzip(基础)或 brotli(更省 10-20%)。CDN 通常自动开。
4. Bundle 分析
npx webpack-bundle-analyzer dist/stats.json
# 看哪些包大,删 / 替换 / 懒加载
运行时优化
React 性能
- React.memo:函数组件浅比较 props 是否变,没变就跳过重渲。
- useMemo / useCallback:稳定引用,避免子组件无谓重渲。
- 虚拟列表:react-window / TanStack Virtual。
- 避免内联函数:每次重渲都创建新函数,触发子组件 memo 失效。
Vue 性能
- v-once:静态内容只渲染一次。
- v-memo:Vue 3.2+,条件性跳过更新。
- shallowRef / shallowReactive:浅响应式,大对象别走深响应式。
监控:RUM(Real User Monitoring)
import { onCLS, onINP, onLCP, onTTFB, onFCP } from 'web-vitals';
function send(metric) {
navigator.sendBeacon('/analytics', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
delta: metric.delta,
url: location.href,
userAgent: navigator.userAgent,
}));
}
onCLS(send);
onINP(send);
onLCP(send);
onTTFB(send);
onFCP(send);
实验室数据(Lighthouse)和真实用户数据(RUM)经常不一样 —— 真实用户的网络 / 设备分布广。两个都监控。Google Search Console 用 CrUX(Chrome User Experience Report)的真实数据评估 SEO。
性能预算
把性能目标写进 CI:
# lighthouse-ci 配置
{
"ci": {
"assert": {
"preset": "lighthouse:no-pwa",
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
}
}
}
}
# PR 检查,性能退步直接拦截
常见反模式
反模式 1:无限滚动 + 不清理。每滚一次加 100 条 DOM,几小时后页面有几万个 DOM 节点,卡得不行。用虚拟列表。
反模式 2:全局 store 每次都全量更新。Redux / Pinia 应该用 selector 精细订阅,而不是整个 store 都监听。
反模式 3:Console.log 在生产。大对象 console.log 性能很差,且开发者工具打开后这些日志占内存。生产构建必须 strip。
反模式 4:CSS-in-JS 过度。某些 CSS-in-JS 库每次渲染都生成新样式注入,性能糟糕。生产场景考虑 zero-runtime 方案(Vanilla Extract、Linaria)或返回普通 CSS。
写在最后
Web 性能不是一次优化完事,是持续过程。把性能监控加进 CI,把性能指标作为产品质量的一部分,你的应用才能长期保持快速。这是和"代码质量""测试覆盖率"同等重要的工程基础设施。
一图看懂
Core Web Vitals 时间轴一图看懂:
—— 别看了 · 2026