一、引言
Web 页面里体积占比最大的永远是图片——一张未优化的首屏图常常比整份 HTML、CSS、JS 还大好几倍。Core Web Vitals 里,LCP 的候选元素绝大多数就是图片或背景图。图片优化因此是性能工程里「投入产出比最高」的一块。
本文讲透四件事:现代图片格式(AVIF / WebP)怎么选、Image CDN 怎么按需转换、响应式图片(srcset / sizes)怎么配、对象存储里的图片处理管线怎么搭,最后给出度量和预算的方法。
二、现代图片格式:AVIF 与 WebP 的取舍
2.1 格式对比
| 格式 | 压缩率 | 浏览器支持 | 特性 |
|---|---|---|---|
| JPEG | 基准 | 全平台 | 有损,照片友好 |
| PNG | 低 | 全平台 | 无损,透明、图标 |
| WebP | 比 JPEG 小 25~35% | 现代浏览器全覆盖 | 有损/无损,透明,动画 |
| AVIF | 比 WebP 再小 20~30% | 除极老浏览器外全覆盖 | AV1 编码,HDR 支持,极强压缩 |
2.2 生产策略:AVIF 优先、WebP 兜底
<picture> 内部顺序很重要:
<source type="image/avif" → 支持 AVIF 的浏览器命中
<source type="image/webp" → 不支持 AVIF 的现代浏览器命中
<img src=".jpg" → 老浏览器兜底
<picture>
<source
type="image/avif"
srcset="/img/hero.avif 1200w, /img/hero@2x.avif 2400w"
sizes="(max-width: 900px) 100vw, 1200px"
>
<source
type="image/webp"
srcset="/img/hero.webp 1200w, /img/hero@2x.webp 2400w"
sizes="(max-width: 900px) 100vw, 1200px"
>
<img
src="/img/hero.jpg"
srcset="/img/hero@2x.jpg 2400w"
width="1200" height="675"
alt="产品主视觉"
loading="lazy"
>
</picture>
心法:AVIF 的压缩率最高但编码慢、解码成本略高;对「照片类大图」收益显著,对「小图标」不值得。策略是大图走 AVIF 优先,小图与动图按需评估。
2.3 编码参数建议
# AVIF:质量 50~70 平衡体积与观感(工具示例)
avifenc --speed 6 --quality 60 --yuv 420 input.jpg -o output.avif
# WebP:质量 75~82
cwebp -q 78 input.jpg -o output.webp
三、Image CDN:按需转换与缓存分发
3.1 为什么需要 Image CDN
传统做法:设计稿 → 手动导出多尺寸 → 各自存文件 → 前端写死 srcset
问题:每个尺寸都要人维护,改一处要重新导出
Image CDN 做法:存一张原图 → URL 参数实时转换 → CDN 缓存分发
收益:一套源图,任意尺寸/格式/质量按需产出
3.2 常见的 Image CDN / 图片处理服务
| 服务 | 形态 | 特点 |
|---|---|---|
| Cloudflare Images | 托管 + CDN | 边缘转换、变体、签名 URL |
| Cloudflare Image Resizing | Workers 内联 | 自建 Worker 里按需转换 |
| Vercel 集成 | 托管 | 与 Next.js Image 原生联动 |
| imgix | 独立 CDN | URL 参数驱动,功能全面 |
| 自建 | R2 + Worker | 完全可控、成本透明 |
3.3 用 Worker + R2 实现「按需 Image CDN」
// worker-image.ts:R2 里存原图,URL 参数驱动转换
export default {
async fetch(request: Request, env: { IMAGES: R2Bucket }): Promise<Response> {
const url = new URL(request.url)
const key = url.pathname.replace(/^\//, '') // 源图 key
const width = url.searchParams.get('w') // 目标宽度
const format = url.searchParams.get('fmt') ?? 'webp' // 目标格式
const object = await env.IMAGES.get(key)
if (!object) return new Response('Not Found', { status: 404 })
// 调用图片处理服务(或自建解码管线)
const resized = await resizeImage(object.body, {
width: Number(width ?? 800),
format,
quality: 78,
})
const cacheKey = `image:${key}:${width}:${format}`
const cache = caches.default
const cached = await cache.match(new Request(`https://cache/${cacheKey}`))
if (cached) return cached
const response = new Response(resized, {
headers: {
'Content-Type': `image/${format}`,
'Cache-Control': 'public, max-age=31536000, immutable',
'CDN-Cache-Control': 'public, max-age=31536000',
},
})
await cache.put(new Request(`https://cache/${cacheKey}`), response.clone())
return response
},
}
细节:转换结果用
immutable缓存策略,因为同一 URL(同一 key+width+format)永远产出同一张图。配合 边缘缓存策略 里的缓存键设计,图片 CDN 的命中率能到 99% 以上。
四、响应式图片:srcset 与 sizes
4.1 srcset 与 sizes 怎么配合
srcset:告诉浏览器「有哪些尺寸可选」(宽度描述符 w)
sizes:告诉浏览器「这个元素在不同视口下实际显示多宽」
浏览器:根据 DPR 与容器宽度,选最合适的源
<img
src="/img/photo.jpg"
srcset="
/img/photo-480.jpg 480w,
/img/photo-800.jpg 800w,
/img/photo-1200.jpg 1200w,
/img/photo-1600.jpg 1600w
"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 1200px"
width="1600" height="900"
alt="风景照片"
>
4.2 尺寸档位怎么定
档位太少:高 DPR 设备浪费带宽
档位太多:CDN 缓存碎片化、源图类型爆炸
常用做法:
取「常见视口 × 常见 DPR」的关键档:
480 / 800 / 1200 / 1600 / 2400
再用 AVIF 压缩率高、多档位成本低的特点兜底
4.3 width / height 必须声明
<!-- 错误:未声明尺寸 → CLS 抖动 -->
<img src="/img/photo.jpg" alt="风景">
<!-- 正确:声明宽高比 → 布局稳定,配合 CSS aspect-ratio -->
<img src="/img/photo.jpg" width="1600" height="900" alt="风景">
心法:图片导致 CLS(累积布局偏移)是新手最容易踩的性能坑。声明
width/height或aspect-ratio,浏览器才能预留空间。这与 前端性能优化:Core Web Vitals 里对 CLS 的度量方法直接对应。
五、对象存储里的图片处理管线
5.1 上传时处理 vs 按需处理
上传时处理:原始图 → 一次性生成多尺寸 → 存对象存储
优点:读时零计算,访问纯静态
缺点:变体类型爆炸、格式迭代要重新批量生成
按需处理:存一张原图 → URL 参数驱动转换 + 缓存
优点:格式迭代零成本(AVIF 出来改个参数)
缺点:首次访问有冷转换延迟
5.2 用 R2 + 队列实现「后处理管线」
// 上传入口:写入原图 + 触发处理任务
async function handleUpload(request: Request, env: { IMAGES: R2Bucket; QUEUE: Queue }): Promise<Response> {
const form = await request.formData()
const file = form.get('file') as File
const key = `originals/${crypto.randomUUID()}-${file.name}`
await env.IMAGES.put(key, file.stream(), { httpMetadata: { contentType: file.type } })
// 异步队列:生成 WebP/AVIF + 多尺寸
await env.QUEUE.send({ key, type: file.type })
return Response.json({ key })
}
// 队列消费者:批量产出变体
export default {
async queue(batch: MessageBatch<{ key: string; type: string }>, env: { IMAGES: R2Bucket }) {
for (const msg of batch.messages) {
const { key, type } = msg.body
const original = await env.IMAGES.get(key)
const variants = await Promise.all(
[
{ width: 480, format: 'webp' },
{ width: 800, format: 'webp' },
{ width: 1200, format: 'avif' },
{ width: 1600, format: 'avif' },
].map((v) => resizeAndStore(env.IMAGES, original.body, key, v)),
)
console.log('variants_done', { key, count: variants.length })
}
},
}
5.3 媒体对象的安全与签名
// 私有桶 + 预签名 URL:临时授权访问原图或高分辨率变体
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { GetObjectCommand } from '@aws-sdk/client-s3'
const url = await getSignedUrl(s3Client, new GetObjectCommand({
Bucket: 'my-media',
Key: 'originals/large.jpg',
}), { expiresIn: 3600 }) // 1 小时有效
六、懒加载、占位与优先级
6.1 原生懒加载与优先级
<!-- 首屏图:高优先级 + fetchpriority 提升 LCP -->
<img src="/img/hero.avif" width="1600" height="900" alt="主视觉"
fetchpriority="high" loading="eager" decoding="async">
<!-- 屏下图:低优先级懒加载 -->
<img src="/img/thumb.jpg" width="400" height="300" alt="缩略图"
loading="lazy" decoding="async">
6.2 占位策略对比
方法一:LQIP(低质量占位)——先加载模糊小图,后换清晰图
实现:模糊图 base64 内联 或 极低分辨率 + CSS blur
方法二:solid color / aspect-ratio 占位
实现:容器 aspect-ratio + 背景色,图到后覆盖
方法三:blurhash 占位
实现:几十字节的哈希 → 服务端/客户端解码成模糊占位
/* aspect-ratio 占位:布局稳定 + 背景色兜底 */
.img-wrap {
aspect-ratio: 16 / 9;
background: #e2e8f0;
overflow: hidden;
}
.img-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s;
opacity: 0;
}
.img-wrap img.loaded { opacity: 1; }
// 监听加载完成,淡入
const img = document.querySelector('img')
img.addEventListener('load', () => img.classList.add('loaded'))
七、性能度量与预算
7.1 度量什么
LCP 元素是不是图片?(是 → 图片优化直接作用于 LCP)
首屏图片总字节数(Transfer Size)
图片请求数
每个图片的 wasted bytes(Lighthouse 会给出)
7.2 用 Lighthouse 与 CRUX 建立预算
# Lighthouse:图片体积占比、优化建议
npx lighthouse https://example.com --only-categories=performance
# 重点看三项:
# - Total Byte Weight:图片占比
# - LCP 候选是图片?
# - Properly size images:尺寸不符的图片
7.3 预算怎么定
预算示例(电商首屏):
首屏图片总字节 ≤ 800KB(含 AVIF 后的理想值)
LCP 图片 ≤ 150KB(AVIF)
图片请求数 ≤ 8
超预算 → 走 Image CDN 降低质量或换格式
心法:图片优化要有「预算制」——没有数字的目标只是口号。把图片字节预算写进 CI,超了就报,图片性能才不会随版本悄悄劣化。
八、总结
图片与媒体优化的完整落地要点:
- 格式分层:AVIF 优先、WebP 兜底、JPEG 老浏览器垫底,大图收益最明显。
- Image CDN 化:存一张原图,URL 参数按需转换,配合 CDN 缓存命中率高。
- 响应式必须配 srcset + sizes:让浏览器按 DPR 与容器宽度选图,避免高 DPR 设备硬塞大图。
- 声明宽高 / aspect-ratio:消灭 CLS,图片加载前后布局稳定。
- 对象存储处理管线:上传时队列后处理出多尺寸变体,或按需转换二选一,别两头都做。
- 懒加载与优先级:首屏
fetchpriority="high",屏下loading="lazy",占位方案保证体验。 - 度量与预算:盯 LCP 图片、首屏字节、请求数,把预算写进 CI。
图片不是「放上去就行」的资源,而是可以量化的性能资产。把格式、转换、响应式、占位、预算这套管线搭起来,图片就从页面最大的包袱变成稳定的性能收益。想了解整体指标如何度量,可回看 Core Web Vitals 深入 的度量方法,图片优化只是其中一环。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。