80% 的 Web 性能问题来自资源加载。 一张未优化的 4K 图片可能消耗比整个 JS Bundle 还多带宽;一次错误的缓存策略可能让用户每次访问都重新下载所有资源。本文覆盖从图片到缓存的全链路优化。
一、图片优化:最大收益点
1.1 现代图片格式对比
| 格式 | 压缩率 vs JPEG | 浏览器支持 | 适用场景 |
|---|---|---|---|
| AVIF | -50%~-70% | Chrome 85+, Firefox 93+, Safari 16+ | 最佳选择,优先使用 |
| WebP | -25%~-35% | 全现代浏览器 | AVIF 的 fallback |
| JPEG XL | -50%~-60% | Chrome 91+(实验性) | 未来潜力,暂不稳定 |
| MozJPEG | -10%~-20% | 全浏览器 | 渐进式 JPEG |
| PNG | — | 全浏览器 | 需要透明/无损 |
| SVG | — | 全浏览器 | 图标、Logo、简单图形 |
1.2 响应式图片:srcset + sizes
<!-- 根据视口宽度自动选择最佳尺寸 -->
<img
srcset="
/images/hero-400.avif 400w,
/images/hero-800.avif 800w,
/images/hero-1200.avif 1200w,
/images/hero-1600.avif 1600w
"
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw
"
src="/images/hero-800.avif"
alt="Hero image"
width="1600"
height="900"
loading="lazy"
decoding="async"
>
sizes 语法:告诉浏览器当前图片在不同媒体查询下的显示宽度,浏览器据此选择最佳 srcset 资源。
1.3 picture 标签:格式回退
<picture>
<!-- 优先尝试 AVIF -->
<source
srcset="/images/photo.avif"
type="image/avif"
>
<!-- 其次 WebP -->
<source
srcset="/images/photo.webp"
type="image/webp"
>
<!-- 最后 JPEG -->
<img
src="/images/photo.jpg"
alt="Photo"
width="800"
height="600"
loading="lazy"
>
</picture>
1.4 图片懒加载
<!-- 原生懒加载(现代浏览器支持 ~95%+) -->
<img src="photo.jpg" loading="lazy" alt="...">
<!-- 优先级控制 -->
<img src="hero.jpg" fetchpriority="high" alt="首屏图">
<img src="below-fold.jpg" fetchpriority="low" loading="lazy" alt="非首屏">
// Intersection Observer(更精细控制,如提前加载阈值)
const imgObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
}, {
rootMargin: '200px 0px', // 提前 200px 开始加载
threshold: 0.01
});
document.querySelectorAll('img.lazy').forEach(img => imgObserver.observe(img));
1.5 图片优化工具链
# CLI 工具
npm install -D sharp imagemin-cli svgo
# sharp:批量转换 + 压缩
npx sharp input.jpg --avif --quality 75 --output output.avif
npx sharp input.jpg --webp --quality 80 --output output.webp
# 生成多尺寸
for w in 400 800 1200 1600; do
npx sharp input.jpg --resize $w --avif --output "output-${w}.avif"
done
# SVG 优化
npx svgo icons/*.svg --pretty --multipass
二、字体加载优化
2.1 字体加载问题
FOIT(Flash of Invisible Text):字体下载前文字不可见
FOUT(Flash of Unstyled Text):先显示回退字体,加载后切换
FOIT 伤害感知性能,FOUT 伤害视觉稳定性(可能导致 CLS)
2.2 优化策略
<!-- 1. 预连接字体 CDN -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 2. 预加载关键字体(仅限首屏必须的 1-2 个) -->
<link rel="preload" href="/fonts/main-400.woff2" as="font" type="font/woff2" crossorigin>
<!-- 3. font-display 策略 -->
<style>
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-weight: 400;
font-style: normal;
/* 推荐:短暂阻塞(100ms)后显示回退字体,加载完成后切换 */
font-display: swap;
}
</style>
<!-- font-display 取值:
auto — 浏览器默认
block — 3s 隐形文字(FOIT)→ 回退
swap — 立即回退(FOUT)→ 加载后切换 ✅ 推荐
fallback — 100ms 隐形 → 回退 → 3s 内未加载则保持回退
optional — 100ms 隐形 → 回退 → 不切换(网络慢时)
-->
2.3 字体子集化(Subsetting)
# 用 glyphhanger 生成只包含使用到的字符的字体
npx glyphhanger https://example.com \
--formats=woff2 \
--subset=/fonts/original.ttf \
--output=/fonts/subset/
# 或用 pyftsubset(fontTools)
pyftsubset font.ttf \
--text="常用汉字列表..." \
--output-file=font-subset.woff2 \
--flavor=woff2
2.4 系统字体栈
/* 最快加载速度:不用网络字体 */
body {
font-family:
-apple-system, BlinkMacSystemFont, /* macOS / iOS */
'Segoe UI', Roboto, /* Windows / Android */
'Helvetica Neue', Arial,
'Noto Sans', 'PingFang SC', /* 中文 */
'Microsoft YaHei', /* Windows 中文 */
sans-serif;
}
三、视频与 iframe 懒加载
3.1 视频优化
<!-- 1. 封面图占位 + 点击加载 -->
<div class="video-placeholder" data-video-id="abc123">
<img src="video-poster.jpg" alt="Video thumbnail" loading="lazy">
<button class="play-button">▶</button>
</div>
<!-- 2. 仅首屏视频 autoplay,其他懒加载 -->
<video
poster="thumbnail.jpg"
preload="none" <!-- 不预加载 -->
controls
loading="lazy"
>
<source src="video.mp4" type="video/mp4">
</video>
<!-- 3. 用 Lighthouse 推荐的 Video 压缩 -->
<!-- ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset fast -c:a aac -b:a 128k output.mp4 -->
3.2 iframe 懒加载
<!-- 原生支持 -->
<iframe src="map.html" loading="lazy" width="600" height="400"></iframe>
<!-- YouTube 嵌入优化:用 facades 模式 -->
<!-- 先显示封面图 + 播放按钮,点击后才加载 iframe -->
<div class="youtube-facade" data-video="dQw4w9WgXcQ">
<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/hqdefault.jpg" alt="...">
<button class="play-btn">▶</button>
</div>
<script>
document.querySelectorAll('.youtube-facade').forEach(el => {
el.addEventListener('click', () => {
const id = el.dataset.video;
el.innerHTML = `<iframe src="https://www.youtube.com/embed/${id}?autoplay=1" ...></iframe>`;
});
});
</script>
四、HTTP 缓存策略
4.1 缓存机制全图
浏览器缓存层级:
┌─────────────────────────────────────────┐
│ 1. Service Worker Cache(最优先) │
│ ← 完全可控,可实现离线访问 │
├─────────────────────────────────────────┤
│ 2. HTTP Cache(Disk / Memory) │
│ ← Cache-Control / ETag / Last-Modified│
├─────────────────────────────────────────┤
│ 3. 协商缓存(304 Not Modified) │
│ ← ETag 对比 │
├─────────────────────────────────────────┤
│ 4. 直连服务器(无缓存) │
└─────────────────────────────────────────┘
4.2 Cache-Control 指令
| 指令 | 含义 | 场景 |
|---|---|---|
max-age=31536000 | 缓存 1 年 | 版本化静态资源(JS/CSS/图片) |
immutable | 内容永不改变 | fingerprint 文件名 |
no-cache | 必须重新验证 | 动态 API 响应 |
no-store | 完全不缓存 | 敏感数据 |
private | 仅浏览器缓存 | 用户专属内容 |
public | CDN 可缓存 | 通用静态资源 |
stale-while-revalidate=86400 | 缓存过期后 1 天内先返回旧值再后台更新 | 非实时数据 |
4.3 静态资源缓存配置
# Nginx 配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|avif|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
}
# 带 hash 的资源(如 main.a3f2b1c.js)→ 永久缓存
# 不带 hash 的 HTML → 不缓存或短时间缓存
location ~* \.html$ {
expires 5m;
add_header Cache-Control "public, must-revalidate";
}
4.4 ETag 与协商缓存
首次请求:
客户端 → GET /api/data
服务端 → 200 OK + ETag: "abc123" + body
后续请求:
客户端 → GET /api/data + If-None-Match: "abc123"
服务端 → 304 Not Modified(body 为空,省带宽)
→ 或 200 OK + 新 ETag(内容变了)
五、CDN 与边缘加速
5.1 CDN 缓存层级
用户请求 → 浏览器缓存 → CDN Edge(PoP)→ CDN Origin Shield → 源站
↓
边缘节点遍布全球,就近响应
静态资源命中 Edge → 延迟 < 50ms
5.2 Cloudflare 缓存规则示例
规则 1:静态资源长期缓存
匹配:URL Path contains ".js" OR ".css" OR ".avif" OR ".webp"
操作:Cache Level = Cache Everything
Edge TTL = 1 month
Browser TTL = 1 year
规则 2:HTML 短期缓存
匹配:URL Path contains ".html" OR Path = "/"
操作:Cache Level = Cache Everything
Edge TTL = 5 minutes
Browser TTL = 5 minutes
规则 3:API 不缓存
匹配:URL Path starts with "/api/"
操作:Cache Level = Bypass
5.3 边缘渲染(Edge SSR)
// Vercel Edge Function:在 CDN 边缘运行,减少 TTFB
export const config = { runtime: 'edge' };
export default async function handler(request: Request) {
const url = new URL(request.url);
// 检查 CDN 缓存
const cache = caches.default;
const cached = await cache.match(request);
if (cached) return cached;
// 边缘渲染
const html = await renderPage(url.pathname);
const response = new Response(html, {
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
}
});
await cache.put(request, response.clone());
return response;
}
六、资源压缩
6.1 Brotli vs Gzip
| 算法 | 压缩率 | CPU 开销 | 浏览器支持 |
|---|---|---|---|
| Brotli(quality=11) | 最高(~20% better than gzip) | 高(压缩慢) | ~98% |
| Gzip | 中等 | 低 | ~100% |
# Nginx 配置(Brotli + Gzip fallback)
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json;
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/javascript application/json;
6.2 构建时预压缩
# 用 Brotli 预压缩静态资源(减少服务器实时压缩开销)
for f in dist/**/*.{js,css,html,json,svg}; do
brotli -q 11 -o "$f.br" "$f" &
gzip -9 -k "$f" &
done
wait
七、关键渲染路径优化
7.1 资源优先级控制
<!-- 最高优先级:关键 CSS -->
<link rel="preload" href="/critical.css" as="style" onload="this.rel='stylesheet'">
<!-- 高优先级:首屏字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- 中优先级:首屏图片 -->
<link rel="preload" href="/hero.avif" as="image" type="image/avif">
<!-- 低优先级(或不需要预加载):非关键资源 -->
<!-- 103 Early Hints(HTTP/2+):在完整响应前发送预加载提示 -->
<!-- 服务端发送:
HTTP/1.1 103 Early Hints
Link: </critical.css>; rel=preload; as=style
Link: </main.js>; rel=preload; as=script
-->
7.2 关键 CSS 内联
<head>
<!-- 只内联首屏必须的 CSS(通常 < 14KB gzip) -->
<style>
/* critical.css 内容 */
.hero { min-height: 100vh; display: flex; ... }
.nav { position: fixed; top: 0; ... }
/* 其他 CSS 异步加载 */
</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>
7.3 脚本加载策略
<!-- async:下载不阻塞,下载完立即执行 -->
<script src="analytics.js" async></script>
<!-- defer:下载不阻塞,DOM 解析完按序执行 ✅ 推荐 -->
<script src="app.js" defer></script>
<!-- module:自动 defer -->
<script type="module" src="app.js"></script>
<!-- type="speculationrules":预渲染下一页(Chrome 103+) -->
<script type="speculationrules">
{
"prerender": [{
"source": "list",
"urls": ["/about", "/contact"]
}]
}
</script>
八、资源优化 Checklist
| 检查项 | 做法 | 预期收益 |
|---|---|---|
| 图片格式 | AVIF > WebP > JPEG + responsive | 50-70% 体积减少 |
| 图片尺寸 | srcset + sizes 或 | 移动端省 60%+ |
| 懒加载 | loading=“lazy” + Intersection Observer | 减少初始请求 |
| 字体 | font-display: swap + subset | 消除 FOIT |
| 缓存策略 | Cache-Control immutable(静态资源) | 重复访问 0 传输 |
| 压缩 | Brotli(构建时预压缩) | 25% 额外减少 |
| CDN | 全球 Edge 节点 + 缓存规则 | TTFB < 100ms |
| 关键渲染 | 关键 CSS 内联 + preload | FCP 提升 |
| 脚本加载 | defer / async / type=“module” | 减少阻塞 |
参考与延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。