博客里图片是唯一的体积炸弹。我的策略很简单:原图永远不进仓库,构建时扫一遍 content/assets/,把每张图压成两个尺寸、两种格式,然后按文件名生成 <picture>。
依赖只有一个:ffmpeg
ffmpeg -version # 需要有 libaom / libsvtav1 或 libwebp
Node 只是负责遍历目录、拼命令、写 HTML 片段。
核心逻辑
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const SIZES = [800, 1600];
for (const file of fs.readdirSync(SRC)) {
if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
const base = path.basename(file, path.extname(file));
for (const w of SIZES) {
// AVIF:质量 42,慢但小
execFileSync("ffmpeg", [
"-y", "-i", path.join(SRC, file),
"-vf", `scale=${w}:-2`,
"-c:v", "libsvtav1", "-crf", "32", "-preset", "6",
path.join(OUT, `${base}-${w}.avif`),
]);
// WebP:兜底
execFileSync("ffmpeg", [
"-y", "-i", path.join(SRC, file),
"-vf", `scale=${w}:-2`,
"-c:v", "libwebp", "-quality", "78",
path.join(OUT, `${base}-${w}.webp`),
]);
}
}
scale=w:-2 的 -2 保证高度是偶数,AVIF 编码器对奇数高度会报错。
生成 picture
<picture>
<source type="image/avif" srcset="/img/shot-800.avif 800w, /img/shot-1600.avif 1600w" sizes="(max-width: 720px) 100vw, 720px">
<source type="image/webp" srcset="/img/shot-800.webp 800w, /img/shot-1600.webp 1600w" sizes="(max-width: 720px) 100vw, 720px">
<img src="/img/shot-800.webp" width="1600" height="900" loading="lazy" decoding="async" alt="截图">
</picture>
width / height 写原图比例,不是实际像素——浏览器靠它算占位,避免布局跳动。sizes 决定它下载哪一档。
效果
一张 2.4 MB 的 4K 截图,压完:
- AVIF 1600w:118 KB
- WebP 1600w:196 KB
- AVIF 800w:47 KB
在 3 Mbps 的机器上,这就是「等 100 秒」和「等 0.5 秒」的区别。
几个坑
- ffmpeg 的 AVIF 编码很吃 CPU,4K 图一张要 3–6 秒。用
-preset 6,别追求极限。 - 文件名不要带空格和中文,
sizes和srcset会很难受。 - 别对 PNG 截图做有损压缩后再当 UI 素材用,文字会糊。截图统一转 WebP 无损,照片用 AVIF 有损。
图片是唯一一种「优化一次、省一辈子」的资源。
COMMENTS留言 (0)
LOGIN REQUIRED