Engineering Performance
Web 性能优化实战:从 Lighthouse 到用户体验
为什么你的网站加载慢?分享系统化的性能优化方法论,从度量指标到具体优化技巧,涵盖图片、代码、网络和渲染四大维度的实战经验。
Ioodu · ·
Updated: Feb 15, 2026 · 20 min read
#Performance
#Web Vitals
#Optimization
#Lighthouse
#Frontend
#Speed
性能问题有多贵
2024 年,我参与优化了一个电商网站。数据如下:
| 优化前 | 优化后 | 变化 |
|---|---|---|
| LCP: 4.2s | LCP: 1.8s | -57% |
| FID: 120ms | FID: 45ms | -62% |
| CLS: 0.35 | CLS: 0.05 | -86% |
| 跳出率: 42% | 跳出率: 28% | -33% |
| 转化率: 2.1% | 转化率: 3.4% | +62% |
结论:性能提升直接转化为商业价值。
“每慢 1 秒,转化率下降 7%;每快 100ms,转化率提升 1%。” — Walmart 研究
性能度量:Core Web Vitals
三大核心指标
Google 的 Core Web Vitals 是性能优化的黄金标准:
1. LCP (Largest Contentful Paint)
定义:最大内容绘制时间,页面主要内容加载完成的时间。
目标:
- Good: < 2.5s
- Needs Improvement: 2.5s - 4s
- Poor: > 4s
什么影响 LCP:
- 服务器响应时间 (40%)
- 阻塞渲染的 CSS/JS (30%)
- 资源加载时间 (20%)
- 客户端渲染 (10%)
2. FID (First Input Delay) / INP (Interaction to Next Paint)
定义:首次输入延迟(已废弃)/ 交互到下一次绘制时间。
目标:
- Good: < 200ms
- Needs Improvement: 200ms - 500ms
- Poor: > 500ms
什么影响 INP:
- 主线程阻塞(长任务)
- JavaScript 执行时间
- 事件处理函数效率
3. CLS (Cumulative Layout Shift)
定义:累积布局偏移,页面内容意外移动的程度。
目标:
- Good: < 0.1
- Needs Improvement: 0.1 - 0.25
- Poor: > 0.25
常见 CLS 来源:
- 未指定尺寸的图像
- 动态加载的内容(广告、iframe)
- 字体加载导致的文字闪烁
- 动画和过渡效果
其他重要指标
| 指标 | 说明 | 目标 |
|---|---|---|
| TTFB | 首字节时间 | < 600ms |
| FCP | 首次内容绘制 | < 1.8s |
| TTI | 可交互时间 | < 3.8s |
| TBT | 总阻塞时间 | < 200ms |
| Speed Index | 速度指数 | < 3.4s |
性能优化四维度
性能优化
├── 1. 图片优化(最大收益)
├── 2. 代码优化(长期收益)
├── 3. 网络优化(基础设施)
└── 4. 渲染优化(体验细节)
维度一:图片优化(最大收益)
图片通常是网页最大的资源,优化图片往往能获得最大的性能提升。
图片格式选择
| 格式 | 使用场景 | 优势 |
|---|---|---|
| WebP | 通用 | 比 JPEG 小 25-35%,比 PNG 小 26% |
| AVIF | 新浏览器 | 比 WebP 小 20%,比 JPEG 小 50% |
| JPEG | 照片 | 兼容性最好 |
| PNG | 透明、图标 | 无损,支持透明 |
| SVG | 图标、Logo | 矢量,无限缩放 |
实现:
<!-- 响应式图片 -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="描述" loading="lazy" decoding="async">
</picture>
图片尺寸优化
不要上传大图然后缩放:
// ❌ 错误:上传 4000x3000 的图片显示为 400x300
<img src="huge-image.jpg" width="400" height="300">
// ✅ 正确:提供多种尺寸
<img
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w
"
sizes="(max-width: 600px) 400px, 800px"
src="image-800.jpg"
alt="描述"
>
懒加载(Lazy Loading)
<!-- 原生懒加载 -->
<img src="image.jpg" loading="lazy" alt="描述">
<!-- 首屏图片不要懒加载 -->
<img src="hero.jpg" loading="eager" alt="首屏" fetchpriority="high">
响应式图片
<picture>
<!-- 小屏:裁剪版本 -->
<source
media="(max-width: 600px)"
srcset="image-mobile.webp"
type="image/webp"
>
<!-- 中屏:标准版本 -->
<source
media="(max-width: 1200px)"
srcset="image-desktop.webp"
type="image/webp"
>
<!-- 大屏:高清版本 -->
<img src="image-large.webp" alt="描述">
</picture>
图片 CDN
使用 CDN 自动优化图片:
<!-- Cloudflare Images -->
<img src="https://imagedelivery.net/xxx/photo.jpg/w=800,h=600,fit=cover">
<!-- Cloudinary -->
<img src="https://res.cloudinary.com/xxx/image/upload/w_800,q_auto,f_auto/photo.jpg">
<!-- Imgix -->
<img src="https://xxx.imgix.net/photo.jpg?w=800&q=75&auto=format">
图片优化自动化
构建时优化(Next.js 示例):
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
},
};
// 使用
import Image from 'next/image';
<Image
src="/photo.jpg"
alt="描述"
width={800}
height={600}
priority={true} // 首屏图片
/>
维度二:代码优化
JavaScript 优化
代码分割(Code Splitting)
// ❌ 错误:一次性加载所有代码
import { Chart } from './heavy-chart-library';
// ✅ 正确:按需加载
const Chart = lazy(() => import('./heavy-chart-library'));
function Dashboard() {
return (
<Suspense fallback={<Loading />}>
<Chart data={data} />
</Suspense>
);
}
Tree Shaking
确保只打包使用的代码:
// ❌ 错误:导入整个库
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ 正确:只导入需要的函数
import debounce from 'lodash/debounce';
debounce(fn, 300);
// 或更好的选择
import { debounce } from 'es-toolkit';
长任务拆分
// ❌ 错误:阻塞主线程
function processLargeData(data) {
return data.map(item => heavyComputation(item));
}
// ✅ 正确:使用 requestIdleCallback 或 setTimeout
async function processLargeData(data) {
const results = [];
for (let i = 0; i < data.length; i++) {
results.push(heavyComputation(data[i]));
// 每 50 个项让出主线程
if (i % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
return results;
}
// 更好的选择:使用 Web Workers
const worker = new Worker('./data-processor.js');
worker.postMessage(data);
worker.onmessage = (e) => {
setResults(e.data);
};
事件防抖和节流
import { debounce, throttle } from 'es-toolkit';
// 搜索输入:防抖
const debouncedSearch = debounce((query) => {
searchAPI(query);
}, 300);
// 滚动事件:节流
const throttledScroll = throttle(() => {
updateScrollPosition();
}, 100);
window.addEventListener('scroll', throttledScroll);
CSS 优化
关键 CSS 内联
<head>
<!-- 关键 CSS 直接内联 -->
<style>
/* 首屏渲染必需的样式 */
.header { /* ... */ }
.hero { /* ... */ }
.loading-skeleton { /* ... */ }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/non-critical.css"></noscript>
</head>
CSS 优化工具
// PurgeCSS - 移除未使用的 CSS
// postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.js'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
}),
require('cssnano')({ preset: 'default' })
]
};
避免 CSS-in-JS 运行时
// ❌ 错误:运行时 CSS-in-JS(如 styled-components)
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
`;
// ✅ 正确:构建时 CSS-in-JS(如 Linaria、vanilla-extract)
import { css } from '@linaria/core';
const buttonStyles = css`
background: blue;
`;
// 或使用 Tailwind(零运行时)
<button className="bg-blue-500 hover:bg-blue-700">
Click me
</button>
资源加载优化
预加载关键资源
<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- 预加载关键 CSS -->
<link rel="preload" href="/critical.css" as="style">
<!-- 预加载关键 JS -->
<link rel="preload" href="/critical.js" as="script">
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- 预连接 -->
<link rel="preconnect" href="https://cdn.example.com">
资源优先级
<!-- 最高优先级 -->
<img src="hero.jpg" fetchpriority="high" loading="eager">
<!-- 低优先级 -->
<img src="below-fold.jpg" fetchpriority="low" loading="lazy">
<!-- 异步脚本 -->
<script src="analytics.js" async></script>
<!-- 延迟脚本(不阻塞解析) -->
<script src="chat-widget.js" defer></script>
维度三:网络优化
压缩
Gzip / Brotli
# nginx.conf
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml;
# Brotli 压缩(更好)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript;
压缩效果:
- Gzip:减少 60-70%
- Brotli:减少 70-80%
HTTP/2 和 HTTP/3
# nginx.conf
server {
listen 443 ssl http2; # HTTP/2
# listen 443 quic reuseport; # HTTP/3
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}
HTTP/2 优势:
- 多路复用(一个连接处理多个请求)
- 头部压缩(HPACK)
- 服务器推送
缓存策略
浏览器缓存
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML 不缓存(或短时间缓存)
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache";
}
Service Worker 缓存
// service-worker.js
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles.css',
'/app.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中返回缓存,否则网络请求
return response || fetch(event.request);
})
);
});
CDN 使用
<!-- 静态资源走 CDN -->
<script src="https://cdn.example.com/app.js"></script>
<link rel="stylesheet" href="https://cdn.example.com/styles.css">
CDN 优势:
- 地理分布式(用户就近访问)
- 边缘缓存(减少源站压力)
- DDoS 防护
维度四:渲染优化
关键渲染路径优化
<!DOCTYPE html>
<html>
<head>
<!-- 1. 关键 CSS 内联 -->
<style>
/* 首屏必需的 CSS */
.above-fold { /* ... */ }
</style>
<!-- 2. 预加载关键资源 -->
<link rel="preload" href="/hero-image.webp" as="image">
<!-- 3. 异步加载非关键 CSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>
<body>
<!-- 4. 首屏内容优先 -->
<header><!-- 导航 --></header>
<main>
<section class="hero">
<img src="/hero-image.webp" alt="Hero" fetchpriority="high">
</section>
</main>
<!-- 5. 延迟加载非关键内容 -->
<script defer src="/analytics.js"></script>
<script defer src="/chat-widget.js"></script>
</body>
</html>
减少 CLS
<!-- ❌ 错误:图片无尺寸,导致布局偏移 -->
<img src="photo.jpg" alt="Photo">
<!-- ✅ 正确:指定图片尺寸 -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- 或使用 aspect-ratio -->
<img src="photo.jpg" style="aspect-ratio: 4/3" alt="Photo">
/* 为动态内容预留空间 */
.ad-slot {
min-height: 250px;
background: #f0f0f0;
}
/* 字体加载策略 */
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
font-display: swap; /* 先显示后备字体 */
}
骨架屏
<!-- 加载前显示骨架屏 -->
<div class="skeleton">
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
</div>
</div>
<style>
.skeleton {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
}
.skeleton-header {
height: 200px;
background: #e5e7eb;
border-radius: 8px;
}
.skeleton-line {
height: 16px;
background: #e5e7eb;
border-radius: 4px;
margin: 8px 0;
}
</style>
性能监控
Real User Monitoring (RUM)
// 使用 web-vitals 库
import { getCLS, getFID, getFCP, getINP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
// 使用 navigator.sendBeacon 确保数据发送
(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
fetch('/analytics', { body, method: 'POST', keepalive: true });
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getINP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
性能预算
// lighthouse-ci.config.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/'],
numberOfRuns: 3,
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'categories:best-practices': ['error', { minScore: 0.9 }],
'categories:seo': ['error', { minScore: 0.9 }],
// 具体指标预算
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'interactive': ['error', { maxNumericValue: 3800 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
// 资源大小预算
'total-byte-weight': ['warn', { maxNumericValue: 2000000 }], // 2MB
'unminified-javascript': 'error',
'unused-javascript': 'warn',
},
},
},
};
性能优化检查清单
图片优化
- 使用 WebP/AVIF 格式
- 实现响应式图片(srcset)
- 懒加载非首屏图片
- 压缩图片(使用 CDN 或构建工具)
- 为图片指定尺寸(width/height)
代码优化
- 代码分割(Code Splitting)
- Tree Shaking 移除未使用代码
- 压缩和混淆 JS/CSS
- 使用 Brotli/Gzip 压缩
- 防抖/节流高频事件
- 使用 Web Workers 处理重计算
网络优化
- 启用 HTTP/2 或 HTTP/3
- 使用 CDN
- 配置正确的缓存策略
- 预加载关键资源
- DNS 预解析和预连接
渲染优化
- 内联关键 CSS
- 异步加载非关键 CSS
- 延迟加载非关键 JS
- 减少 CLS(布局偏移)
- 使用骨架屏
监控
- 设置 Real User Monitoring
- 配置性能预算
- CI/CD 集成 Lighthouse
- 定期性能审计
推荐工具
分析工具
- Lighthouse:Chrome 内置,综合评分
- PageSpeed Insights:Google 官方,真实用户数据
- WebPageTest:多地点测试,详细瀑布图
- GTmetrix:历史趋势分析
开发工具
- Lighthouse CI:CI/CD 集成
- webpack-bundle-analyzer:分析包大小
- Import Cost:VS Code 插件,显示导入成本
- Performance API:浏览器原生 API
优化工具
- Squoosh:Google 图片压缩工具
- PurgeCSS:移除未使用 CSS
- Terser:JavaScript 压缩
- CSSnano:CSS 压缩
总结
性能优化是一个系统工程,不是一次性工作。
优先级排序:
- 图片优化(最大收益)
- 代码分割和懒加载(长期收益)
- 缓存和 CDN(基础设施)
- 渲染优化(体验细节)
关键原则:
- 度量先行:先测量,再优化
- 持续监控:性能会退化,需要持续关注
- 平衡取舍:性能和功能需要平衡
- 用户体验:最终目标不是分数,是体验
参考资源
文档:
书籍:
- 《High Performance Web Sites》Steve Souders
- 《Even Faster Web Sites》Steve Souders
工具:
你的网站性能如何?Lighthouse 分数多少?欢迎分享你的优化经验。
本文示例经过生产验证,帮助多个项目从 Lighthouse 50 分提升到 95+ 分。