背景:博客项目搬家后首页封面加载慢
博客从国外 Netlify 迁回国内一台轻量云服务器。迁移完打开首页,最直观的感受就是慢:头像、文章封面要等好几秒才出来,体验很不好。
优化效果:体积与加载时间对比
| 项目 | 压缩前 | 压缩后 |
|---|---|---|
| 单张封面 | 3.0~4.45MB | 24~78KB |
| 全部 13 张封面合计 | 约 40MB | 约 0.5MB |
平均压缩率 99%,省了 34.6MB。加载时间按服务器 3Mbps 出口带宽粗算,一张 4.45MB 的图要下 11 秒多,压完之后只要 0.08 秒,差了两个数量级。
根因分析:瓶颈在封面原图
排查思路其实很简单:先对比,再测量。
首页有两个图片区,一个是「人生瞬间」相册,一个是文章封面。相册页秒开,封面卡。相册图走 Cloudflare R2 的签名 URL,封面存在服务器本地。CDN 快、源站慢,问题基本锁定在源站出去的资源上。
然后 ls -lh dist/client/covers/ 一看封面大小:全是 3~4MB 的 webp。个人轻量服务器的出口带宽就那么大,一张 4MB 的图光传输就要好几秒,首页十几张图一起拉,白屏几秒是必然的。
再细想:这些封面是 AI 生成、模型导出时直接拿出来的,没做过体积管理。出图默认高分辨率、全质量保真,丢进项目时也没设任何上限。之前部署在 Netlify 上有 CDN 兜底,图再大也感觉不明显;一迁回自己的服务器,问题就暴露了。
实现方案:基于 sharp 的批量压缩
sharp 是 Node.js 生态里的图片处理库,底层是 C 写的 libvips,做缩放、格式转换、压缩这类操作又快又省内存。项目 package.json 的 dependencies 里本来就有,估计是建站时某个依赖带进来的,正好拿来用,不用再引入新东西。
写脚本前先考虑清楚三个问题:
- 封面需要多宽:封面在文章卡片、页面顶部横幅这些位置,实际渲染宽度不会超过 1200px,所以「宽最多 1200」。
- 用哪种格式:同样的内容 webp 比 jpeg 更小、jpeg 又比 png 小,封面图应该优先 webp,按扩展名分流处理。
- 质量压到多少:给每个格式一个「看不出糊」的保守值,webp 72、jpeg 75、png 走最优压缩。
概括起来,三步走:定尺寸上限 → 定格式策略 → 定质量档位。
捋清楚后脚本如下。完整脚本在 scripts/compress-covers.mjs,核心逻辑不长:
const meta = await sharp(p).metadata();
const width = Math.min(meta.width || 1200, 1200);
if (f.toLowerCase().endsWith('.webp')) {
buf = await sharp(p).resize({ width, withoutEnlargement: true }).webp({ quality: 72 }).toBuffer();
} else if (f.toLowerCase().endsWith('.png')) {
buf = await sharp(p)
.resize({ width, withoutEnlargement: true })
.png({ compressionLevel: 9 })
.toBuffer();
} else {
buf = await sharp(p)
.resize({ width, withoutEnlargement: true })
.jpeg({ quality: 75, mozjpeg: true })
.toBuffer();
}
几处代码的含义:
sharp(p).metadata():读取原图元信息,拿到原始宽度Math.min(meta.width || 1200, 1200):取「原图宽和 1200 的较小值」,原图本来就小于 1200 就不动尺寸;resize里的withoutEnlargement: true再兜一层,保证小图不会被放大- 三个分支按扩展名分流,各自走对应格式的压缩参数
- 压完
fs.writeFileSync(p, buf)直接覆盖原文件,循环里把每张图的前后体积打印出来,最后汇总总共省了多少
跑一遍看输出,确认每张都正常缩小,然后 astro build 验证构建产物,部署后复测首页,加载肉眼可见地从「等几秒」变成「秒开」。
压完之后的图和之前比,除了体积,肉眼上几乎没有区别,不用担心没法浏览。原因有两点:
- 宽度上限够用:文章列表卡片、页面顶部横幅实际渲染宽度只有几百到一千像素出头,压之前那份两三千像素的原图,浏览器根本用不到那么多细节。
- 质量档位合适:质量 72 是试过的甜点位,纯色、渐变这些大面积区域几乎无损,只有文字边缘这类高频细节理论上会有一点点差异,但在封面那么小的展示尺寸下看不出来。
说白了,压缩只是把「用不到的分辨率」和「肉眼看不出的冗余像素」去掉了,留下的刚好是展示要用的部分。
权衡取舍:压缩边界与维护成本
- 为什么不压到更狠:质量 72 是「看不太出来损失」和「体积」之间的平衡点。压到 50 体积还能小一截,但文字边缘、渐变会开始糊,封面这种大图放首页一眼就能看出问题。
- 原地覆盖,原图没了:脚本直接覆盖源文件,压缩后原高清图不可恢复。以后要做更高清的版本(比如 2x 屏),得重新找原图。稳妥做法是先备份再覆盖,或者输出到另一个目录。我当时图省事直接覆盖了,只能接受这个代价。
- png 压不动:jpg/webp 能到 99% 的压缩率,是因为原图是设计输出,有大量平滑区域可以丢掉冗余;png 那张只从 301KB 压到 78KB。封面图尽量别用 png,同样的内容 png 天生比 webp 大。
- 压图是补救,不是根治:这个脚本是给已有的 13 张图擦屁股。真正该做的是以后生成封面时就按「宽 1200、webp」出图,别让 4MB 的原图进仓库。
经验总结:可复用的优化流程
这次优化的核心不是「用了 sharp」,而是三步:
- 先测量,用快慢页面对比锁定资源,不瞎猜
- 脚本化,一次写好,以后新增封面一条命令跑完
- 用数字收尾,压缩率、节省的字节、加载时间前后对比一目了然
「测量 → 定位 → 脚本化 → 复测」这套路径,对网站这类「慢」的问题基本都能套用:先证明慢在哪,再对症下药。
评论及留言规则
评论提交后需审核通过才会展示,请耐心等待。
请在交流中保持友善、理性和尊重。
你不得利用本站发布、传播或实施以下行为:
评论提交后需审核通过才会展示。什么是 Waline?