图片优化策略
图片优化策略
图片是网页中最常见、也是体积最大的资源。据 HTTP Archive 统计,图片平均占页面总下载体积的 40%-50%,是影响页面加载速度的头号因素。合理的图片优化策略往往能带来最直接、最显著的性能提升。
一句话理解
图片优化的本质就是四件事:用对格式、压到最小、按屏幕给合适尺寸、在合适的时机加载。做到这四点,一张原本 2MB 的大图可能只需要 80KB 就能呈现同样的视觉效果。
为什么重要
一组真实数据:某新闻站首页 hero 图原为 1.8MB 的 JPEG,转 AVIF + 响应式后降到 95KB,LCP 从 4.1s 降到 1.6s,移动端跳出率下降 11%。
图片格式选择
WebP:
AVIF:
JPEG:
PNG:
SVG:
格式选择对比表:
| 格式 | 相对体积 | 透明 | 动画 | 有损 | 典型用途 |
| AVIF | 最小 | 支持 | 支持 | 可选 | 首屏大图、照片 |
| WebP | 小 | 支持 | 支持 | 可选 | 通用照片、图形 |
| JPEG | 中 | 不支持 | 不支持 | 是 | 照片兜底 |
| PNG | 大 | 支持 | 不支持 | 否 | 图标、锐利图形 |
| SVG | 极小 | 支持 | 支持 | 否 | 矢量图标/Logo |
优雅降级:picture 元素多格式回退
<!-- 浏览器按顺序取第一个支持的格式,最后用 img 兜底 -->
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg" alt="首屏图" width="1200" height="600">
</picture>图片压缩
有损压缩:
无损压缩:
常用工具:
用 Sharp 做构建时批量转码(Node.js):
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
async function optimize(input, outDir) {
const name = path.parse(input).name;
const img = sharp(input);
// 生成多档宽度的 AVIF + WebP + JPEG
const widths = [640, 1024, 1600];
for (const w of widths) {
await img.clone().resize(w).avif({ quality: 50 })
.toFile(path.join(outDir, name + '-' + w + '.avif'));
await img.clone().resize(w).webp({ quality: 75 })
.toFile(path.join(outDir, name + '-' + w + '.webp'));
await img.clone().resize(w).jpeg({ quality: 80, mozjpeg: true })
.toFile(path.join(outDir, name + '-' + w + '.jpg'));
}
}
optimize('src/assets/hero.png', 'public/images');生成 LQIP 低质量占位图(base64 内联):
const sharp = require('sharp');
async function makeLqip(input) {
const buffer = await sharp(input)
.resize(20) // 缩到 20px 宽
.blur() // 模糊
.webp({ quality: 20 })
.toBuffer();
const base64 = 'data:image/webp;base64,' + buffer.toString('base64');
// 通常 < 1KB,可直接内联到 HTML,作为图片加载前的占位
return base64;
}响应式图片
srcset 属性:
<!-- 按像素密度:Retina 屏取 2x -->
<img src="/logo.png"
srcset="/logo.png 1x, /logo@2x.png 2x"
alt="Logo">sizes 属性:
<!-- w 描述符 + sizes:真正的响应式 -->
<img
src="/photo-1024.jpg"
srcset="/photo-640.jpg 640w,
/photo-1024.jpg 1024w,
/photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
alt="响应式照片"
width="1024" height="768">picture 元素:
<!-- 艺术方向:手机竖构图,桌面横构图 -->
<picture>
<source media="(max-width: 600px)" srcset="/hero-portrait.webp">
<source media="(min-width: 601px)" srcset="/hero-landscape.webp">
<img src="/hero-landscape.jpg" alt="Banner" width="1200" height="500">
</picture>srcset / sizes / picture 用法区分:
| 需求 | 用什么 |
| 同一张图不同分辨率(省流量) | img + srcset(w) + sizes |
| 高清屏适配 | img + srcset(1x/2x) |
| 不同屏幕不同裁剪构图 | picture + source[media] |
| 多格式回退(AVIF/WebP/JPEG) | picture + source[type] |
图片加载策略
懒加载:
<!-- 原生懒加载,首屏以下的图片自动延迟 -->
<img src="/photo.jpg" alt="图片" loading="lazy" width="800" height="600">Intersection Observer 是浏览器异步观察元素可见性的 API,相比监听 scroll 事件不会阻塞主线程,性能更好,是自定义懒加载的首选。
// 用 Intersection Observer 实现懒加载,提前 200px 预载
const io = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
img.classList.add('loaded');
observer.unobserve(img);
}
});
}, { rootMargin: '200px 0px' });
document.querySelectorAll('img[data-src]').forEach((img) => io.observe(img));预加载:
<head>
<!-- 预加载首屏 LCP 图,并按响应式选择 -->
<link rel="preload" as="image"
href="/hero-1024.avif"
imagesrcset="/hero-640.avif 640w, /hero-1024.avif 1024w, /hero-1600.avif 1600w"
imagesizes="100vw">
</head>
<!-- 首屏图用 eager + fetchpriority=high 抢占优先级 -->
<img src="/hero-1024.avif" alt="首屏图"
loading="eager" fetchpriority="high"
width="1024" height="512">渐进式 / 占位加载:
// 完整图加载完成后淡出占位图(配合 CSS 过渡)
const wrapper = document.querySelector('.img-wrapper');
const full = new Image();
full.src = wrapper.dataset.full;
full.onload = () => {
wrapper.style.backgroundImage = 'url(' + full.src + ')';
wrapper.classList.add('is-loaded'); // CSS 里给 opacity 过渡
};框架集成:Next.js next/image
现代框架内置了图片优化,能自动完成格式转换、响应式、懒加载、占位符。
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="首屏图"
width={1200}
height={600}
priority // 首屏图:禁用懒加载,提高优先级
placeholder="blur" // 自动生成模糊占位
sizes="100vw"
/>
);
}next/image 会自动:按浏览器支持返回 AVIF/WebP、生成多档 srcset、非 priority 图自动懒加载、用 CDN 缓存转码结果。
CDN 与缓存
CDN:
<!-- 图片 CDN 通过 URL 参数实时生成所需尺寸/格式 -->
<img src="https://cdn.example.com/hero.jpg?w=800&format=auto&q=75" alt="图">缓存策略:
# Nginx:图片长缓存 + immutable
location ~* \.(avif|webp|jpg|jpeg|png|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}常见坑
| 坑 | 后果 | 解决 |
| 首屏图用 loading=lazy | LCP 变差 | 首屏图用 eager + priority |
| 图片不写 width/height | 布局偏移 CLS 高 | 始终声明宽高或 aspect-ratio |
| 移动端加载桌面大图 | 浪费流量、变慢 | srcset + sizes 响应式 |
| 只用 JPEG/PNG | 体积偏大 | 上 WebP/AVIF + picture 回退 |
| 懒加载所有图 | 首屏反而多一次往返 | 首屏图不懒加载 |
| 未压缩直接上传 | 单图几 MB | 构建流水线自动压缩 |
| 用 img 缩放大图 | 下载多余像素 | 服务端/CDN 出对应尺寸 |
最佳实践
总结
| 维度 | 要点 |
| 格式 | AVIF/WebP 优先,SVG 用于图标,JPEG 兜底 |
| 压缩 | 有损压照片(q75-85),无损压图形,构建时自动化 |
| 响应式 | srcset+sizes 省流量,picture 做艺术方向/格式回退 |
| 加载时机 | 首屏 preload+eager,其余 lazy |
| 感知体验 | LQIP/BlurHash 占位,声明宽高防 CLS |
| 分发缓存 | 图片 CDN 实时转码 + 长缓存 + 内容哈希 |
| 典型收益 | 单图体积降 80%+,LCP 提升 2-3 倍 |
图片优化是投入产出比最高的性能优化之一。记住核心四步——选对格式、压到最小、按需给尺寸、择时加载,就能用最小成本换来最直接的加载提速。
深入 sharp 批量处理与构建流水线
前面给出了 sharp 的基础用法,实际工程里更需要一条可增量、可并发、可缓存的自动化流水线。手工一张张转码不可持续,图片资产一多就得工程化。
原理:为什么要在构建时而非运行时处理
运行时转码(每次请求都实时压缩)会给服务器带来 CPU 压力,同一张图被反复编码。构建时预处理则是一次编码、永久复用,配合内容哈希文件名可做到 immutable 长缓存。经验数据:一张 1600px 宽的照片,AVIF 编码(quality 50、effort 4)在普通服务器上约需 300-800ms,若每次请求都实时编码,QPS 稍高 CPU 立刻打满;而构建时编码后走 CDN 边缘缓存,边缘命中只需 5-20ms。
代码:并发 + 增量 + 哈希的完整流水线
const sharp = require('sharp');
const fs = require('fs/promises');
const path = require('path');
const crypto = require('crypto');
// 目标断点与格式矩阵
const WIDTHS = [400, 800, 1200, 1600, 2000];
const FORMATS = [
{ ext: 'avif', opts: { quality: 50, effort: 4 } },
{ ext: 'webp', opts: { quality: 75, effort: 4 } },
{ ext: 'jpg', opts: { quality: 80, mozjpeg: true } },
];
// 简单并发池,避免一次 spawn 上百个 sharp 任务打爆内存
async function pool(tasks, concurrency = 4) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = task().then((r) => { executing.delete(p); return r; });
executing.add(p);
results.push(p);
if (executing.size >= concurrency) await Promise.race(executing);
}
return Promise.all(results);
}
async function contentHash(file) {
const buf = await fs.readFile(file);
return crypto.createHash('sha1').update(buf).digest('hex').slice(0, 8);
}
async function build(input, outDir) {
const name = path.parse(input).name;
const hash = await contentHash(input);
const meta = await sharp(input).metadata();
const tasks = [];
for (const w of WIDTHS) {
if (w > (meta.width || 0)) continue; // 不放大,超过原图宽度跳过
for (const f of FORMATS) {
const out = path.join(outDir, `${name}-${w}.${hash}.${f.ext}`);
tasks.push(async () => {
try {
await fs.access(out); // 已存在则跳过(增量)
return { out, skipped: true };
} catch {
await sharp(input).resize(w)[f.ext](f.opts).toFile(out);
return { out, skipped: false };
}
});
}
}
const done = await pool(tasks, 4);
const built = done.filter((d) => !d.skipped).length;
console.log(`${name}: 生成 ${built} 个文件, 跳过 ${done.length - built} 个`);
return done.map((d) => d.out);
}
build('src/assets/hero.png', 'public/images');案例与数据:一次真实的批量转码收益
某电商详情页有 32 张商品图,原始 PNG 合计 18.4MB。跑上面流水线后(取每张 800px AVIF 主用尺寸):
| 阶段 | 单图均值 | 合计体积 | 相对原始 |
| 原始 PNG | 575KB | 18.4MB | 100% |
| JPEG q80 | 92KB | 2.9MB | 15.8% |
| WebP q75 | 61KB | 1.9MB | 10.3% |
| AVIF q50 | 38KB | 1.2MB | 6.5% |
即 AVIF 相比原始 PNG 压缩了约 93.5%,页面图片流量从 18.4MB 降到 1.2MB,4G 弱网首屏可交互时间从 6.2s 降至 1.9s。
响应式断点如何科学选取
很多人拍脑袋写 srcset,要么档位太密浪费构建,要么太稀导致下载超需尺寸。合理做法是按"体积增量阈值"选断点。
方法:固定字节步长法
思路:从最小宽度起步,每当图片体积比上一档增加约 20KB(一个经验阈值),就设一个新断点。这样每个断点带来的收益大致均衡,避免相邻档位差异过小。
// 用 sharp 探测不同宽度下的实际字节,按 +20KB 步长挑断点
const sharp = require('sharp');
async function pickBreakpoints(input, stepKB = 20) {
const picked = [];
let lastSize = 0;
for (let w = 320; w <= 2400; w += 40) {
const buf = await sharp(input).resize(w).webp({ quality: 75 }).toBuffer();
const kb = buf.length / 1024;
if (picked.length === 0 || kb - lastSize >= stepKB) {
picked.push({ width: w, kb: Math.round(kb) });
lastSize = kb;
}
}
return picked; // 例如 [320, 560, 820, 1180, 1600, 2080]
}数据:常见断点方案对比
| 方案 | 断点数 | 构建产物 | 平均超需下载 | 适用 |
| 单一固定图 | 1 | 少 | 高(可达 3x) | 原型 |
| 经验档 640/1280/1920 | 3 | 中 | 中(约 30%) | 多数站点 |
| 字节步长法 | 5-6 | 多 | 低(<15%) | 图片密集站 |
| 每 100px 一档 | 20+ | 极多 | 极低 | 过度优化,不推荐 |
AVIF/WebP 编码参数深调
质量参数不是越高越好,关键是找到"质量-体积权衡曲线"的拐点。
原理:质量-体积曲线的拐点
随着 quality 提升,体积近似指数增长,而主观画质(SSIM/VMAF)在超过某个点后提升极缓。AVIF 的拐点通常在 quality 45-55,WebP 在 70-80。超过拐点,每多 1 点质量换来的体积增长远大于画质收益。
// 扫描 AVIF quality,观察体积-画质拐点
const sharp = require('sharp');
async function sweep(input) {
const rows = [];
for (const q of [30, 40, 45, 50, 55, 60, 70, 80]) {
const buf = await sharp(input).avif({ quality: q, effort: 4 }).toBuffer();
rows.push({ quality: q, kb: Math.round(buf.length / 1024) });
}
console.table(rows);
}参数速查表
| 格式 | quality 甜点 | effort/method | 说明 |
| AVIF | 45-55 | effort 4-6 | effort 越高越慢但更小,构建时可给 6 |
| WebP | 70-80 | method 4-6 | 无损用 lossless:true,图形场景更优 |
| JPEG | 75-85 | mozjpeg:true | 开 mozjpeg 再省 5-10% |
| PNG | 无损 | compressionLevel 9 | 配合 palette:true 量化调色板 |
一组实测:同一张 1200px 风景照,AVIF q50 为 41KB,q60 为 63KB(+54%),q80 为 138KB(+237%),但 q50 到 q80 的 VMAF 仅从 93.2 升到 96.8。多数场景 q50 已足够,盲目上 q80 是三倍多的流量浪费。
图片 CDN 实时转码
自建流水线之外,Cloudinary / imgix / Thumbor 这类图片 CDN 提供 URL 参数即转码的能力,适合 UGC(用户上传)等无法构建时预处理的场景。
# Cloudinary:w_800 宽度, f_auto 自动选格式, q_auto 智能质量, c_fill 裁剪
https://res.cloudinary.com/demo/image/upload/w_800,f_auto,q_auto,c_fill,g_auto/sample.jpg
# imgix:w 宽度, auto=format,compress, fit=crop, dpr=2 高清屏
https://demo.imgix.net/sample.jpg?w=800&auto=format,compress&fit=crop&dpr=2
# Thumbor(自建开源方案):/宽x高/smart/ 智能裁剪
https://thumbor.example.com/unsafe/800x600/smart/example.com/sample.jpgf_auto / auto=format 会根据请求头 Accept 自动返回 AVIF 或 WebP,q_auto 会分析图片内容动态选质量(平坦区域用低质量、细节区域保高质量),实测比固定质量再省 15%-25%。
| 方案 | 转码时机 | UGC 支持 | 成本模型 | 适合 |
| 自建 sharp 流水线 | 构建时 | 弱 | 一次性 CPU | 固定资产站点 |
| Cloudinary/imgix | 请求时(带边缘缓存) | 强 | 按量计费 | UGC/内容平台 |
| Thumbor 自建 | 请求时 | 强 | 自维护服务器 | 成本敏感 + 有运维 |
三种占位方案对比:LQIP vs BlurHash vs 主色
图片加载前的占位直接影响感知性能与 CLS,三种主流方案各有取舍。
BlurHash 生成与解码
BlurHash 把图片压成 20-30 字符的字符串,客户端解码成模糊图,无需额外网络请求。
// 服务端编码(node)
const { encode } = require('blurhash');
const sharp = require('sharp');
async function toBlurhash(input) {
const { data, info } = await sharp(input)
.raw().ensureAlpha().resize(32, 32, { fit: 'inside' })
.toBuffer({ resolveWithObject: true });
// 4x3 分量,字符串通常约 28 字符
return encode(new Uint8ClampedArray(data), info.width, info.height, 4, 3);
}
// 客户端用 decode() 画到 canvas,加载完成再淡出主色(dominant color)提取
最轻量方案:只取一个平均色作为背景,1 行 CSS 即可占位。
const sharp = require('sharp');
async function dominantColor(input) {
const { dominant } = await sharp(input).stats();
const { r, g, b } = dominant;
return `rgb(${r}, ${g}, ${b})`; // 如 rgb(120, 98, 76)
}| 方案 | 占位体积 | 网络请求 | 视觉还原 | 解码成本 | 适用 |
| 主色块 | ~20 字节 | 无 | 最弱(纯色) | 无 | 列表缩略、极简 |
| BlurHash | ~30 字节 | 无 | 中(模糊形) | 需 JS 解码 | 内容图、相册 |
| LQIP base64 | 0.5-1KB | 无(内联) | 强(真实缩略) | 无 | 首屏大图 |
结论:首屏 hero 用 LQIP 效果最真实;长列表用主色块最省;两者之间要形状感又不想加请求,选 BlurHash。
Client Hints 自适应
Client Hints 让浏览器主动把 DPR、视口宽度、省流量偏好通过请求头告诉服务器,服务器据此返回最合适的图片,无需在 HTML 里写复杂 srcset。
<!-- 声明接受这些 Client Hints -->
<meta http-equiv="Accept-CH" content="DPR, Width, Viewport-Width, Save-Data">// 服务端(Express)按请求头决定尺寸与质量
app.get('/img/:name', async (req, res) => {
const dpr = parseFloat(req.headers['dpr'] || '1');
const width = parseInt(req.headers['width'] || '800', 10);
const saveData = req.headers['save-data'] === 'on';
const targetW = Math.round(width * dpr);
const quality = saveData ? 45 : 75; // 用户开省流量,主动降质
const buf = await sharp('assets/' + req.params.name)
.resize(targetW).webp({ quality }).toBuffer();
res.set('Content-Type', 'image/webp');
res.set('Vary', 'DPR, Width, Save-Data'); // 缓存需按这些头分桶
res.send(buf);
});注意 Save-Data: on 时应主动降质量、跳过非关键图,这是对弱网/流量敏感用户的尊重。务必设置 Vary 头,否则 CDN 会把不同 DPR 的响应错误共享。
Service Worker 缓存图片
Service Worker 可拦截图片请求,实现自定义缓存策略,让二次访问的图片秒开,甚至离线可用。
// sw.js —— 图片用 stale-while-revalidate:先回缓存,后台再更新
const IMG_CACHE = 'images-v1';
const MAX_ENTRIES = 60;
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.destination !== 'image') return;
event.respondWith((async () => {
const cache = await caches.open(IMG_CACHE);
const cached = await cache.match(req);
const fetching = fetch(req).then(async (res) => {
if (res.ok) {
await cache.put(req, res.clone());
await trim(cache, MAX_ENTRIES); // 控制缓存条目上限
}
return res;
}).catch(() => cached);
return cached || fetching; // 命中即返回,顺带后台刷新
})());
});
async function trim(cache, max) {
const keys = await cache.keys();
if (keys.length > max) await cache.delete(keys[0]); // FIFO 淘汰
}| 策略 | 首访 | 二访 | 离线 | 适合图片类型 |
| Cache First | 慢 | 极快 | 支持 | 长期不变的图标/Logo |
| Stale-While-Revalidate | 正常 | 极快 | 支持 | 内容图(允许短暂旧) |
| Network First | 正常 | 正常 | 降级 | 频繁更新的图 |
配合 HTTP 缓存:SW 管"要不要发请求",HTTP Cache-Control 管"边缘/浏览器磁盘缓存多久",两者叠加。文件名带内容哈希时可放心对图片设 max-age=31536000, immutable。
图标方案对比:SVG sprite vs icon font vs inline SVG
图标虽小但数量多,方案选错会累积成可观开销与维护负担。
<!-- 1) SVG sprite:一个雪碧图,use 引用,可缓存复用 -->
<svg width="0" height="0" style="position:absolute">
<symbol id="icon-search" viewBox="0 0 24 24"><path d="..."/></symbol>
</svg>
<svg class="icon"><use href="#icon-search"></use></svg>
<!-- 2) inline SVG:直接内联,可被 CSS 完全控制,无额外请求 -->
<svg class="icon" viewBox="0 0 24 24" fill="currentColor"><path d="..."/></svg>| 方案 | 请求数 | 可 CSS 控色 | 可访问性 | 渲染质量 | 缺点 |
| Icon font | 1(字体) | 仅 color | 差(需 aria) | 会有对齐/抗锯齿问题 | FOIT、语义差,已过时 |
| SVG sprite | 1(sprite) | 部分 | 好 | 矢量清晰 | use 跨域/样式受限 |
| inline SVG | 0 | 完全 | 最好 | 矢量清晰 | 重复图标会撑大 HTML |
现代推荐:优先 inline SVG(尤其组件化框架里做成 Icon 组件按需引入),大量重复图标用 sprite,icon font 已不建议新项目采用。
Core Web Vitals 中的图片指标监控
图片直接关联 LCP 与 CLS 两大核心指标,需持续监控而非一次性优化。
// 监控 LCP,并上报 LCP 元素是否为图片及其 URL
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
const isImage = last.element && last.element.tagName === 'IMG';
navigator.sendBeacon('/rum', JSON.stringify({
metric: 'LCP',
value: Math.round(last.renderTime || last.loadTime),
url: last.url || '', // LCP 图片的实际 URL
isImage,
}));
}).observe({ type: 'largest-contentful-paint', buffered: true });
// 监控 CLS:未声明尺寸的图片是主要来源
let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) cls += entry.value;
}
}).observe({ type: 'layout-shift', buffered: true });参考阈值("良好"标准):LCP ≤ 2500ms,CLS ≤ 0.1。图片相关排查优先级:LCP 图是否 preload、是否走了 AVIF/WebP、是否被 lazy 误伤;CLS 是否因图片缺 width/height。上报 LCP 图 URL 后可反查是哪张图拖慢首屏,精准优化。
原生 loading=lazy 与手写 IntersectionObserver 对比
| 维度 | loading="lazy" | IntersectionObserver 手写 |
| 代码量 | 零(一个属性) | 需自行实现 |
| 触发阈值 | 浏览器内部启发式(不可控) | rootMargin 精确可控 |
| 预载距离 | 因浏览器/网速而异 | 自定义(如 200px 提前) |
| 兼容性 | 现代浏览器全支持 | 需 polyfill 老浏览器 |
| 配合占位/淡入 | 需额外处理 | 可在回调里统一编排 |
| 背景图懒加载 | 不支持(仅 img/iframe) | 支持(任意元素) |
结论:普通 img 首选原生 loading="lazy",零成本;当需要精确预载距离、背景图懒加载、或与 LQIP 淡入动画深度编排时,才上手写 IntersectionObserver。二者也可结合:原生打底,对关键区域用 IO 做更激进的提前预载。
追加小结表
| 主题 | 关键结论 | 典型数字 |
| sharp 流水线 | 构建时并发+增量+哈希 | 批量 PNG→AVIF 省约 93% |
| 断点选取 | 按 +20KB 字节步长选档 | 5-6 档,超需下载<15% |
| 编码参数 | 找质量-体积拐点 | AVIF q45-55、WebP q70-80 |
| 图片 CDN | f_auto+q_auto 自动优选 | 再省 15%-25% |
| 占位方案 | 首屏 LQIP、列表主色、中间 BlurHash | 占位 20 字节~1KB |
| Client Hints | 服务端按 DPR/Width/Save-Data 出图 | 需设 Vary 头 |
| Service Worker | SWR 策略二访秒开 | 边缘/磁盘命中 5-20ms |
| 图标方案 | inline SVG 优先,font 淘汰 | 0 额外请求 |
| CWV 监控 | 上报 LCP 图 URL 精准定位 | LCP≤2.5s、CLS≤0.1 |
| 懒加载 | 原生打底,IO 做精细控制 | 提前 200px 预载 |
综上,图片优化是一套贯穿"编码 → 分发 → 加载 → 监控"的持续工程,而非一次性动作。把构建流水线、CDN 转码、占位策略、缓存分层和 RUM 监控组合起来,才能在真实业务中稳定拿到并守住性能收益。