SPUD NEST // 001 薯窝
登录
A TINY BATCH IMAGE PIPELINE

写一个批量压图的小脚本

博客里图片是唯一的体积炸弹。我的策略很简单:原图永远不进仓库,构建时扫一遍 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 有损。

图片是唯一一种「优化一次、省一辈子」的资源。

END OF ENTRY // 005

COMMENTS留言 (0)

LOGIN REQUIRED
LOADING…