ARTICLE 2026—0731—04FOLIO 2026—0731—04ACRETIONDISKARCHIVUM PERSONAE

博客图片压缩优化技术报告:文章加速加载、画廊保留原图

曾为文章正文图片引入构建期 WebP 压缩(2MB → 63KB)。该方案已于 2026-08-02 下线,图片迁移至独立图床仓库 + jsDelivr CDN;本文保留作历史技术记录,新方案见文末附录。

博客图片压缩优化技术报告

⚠️ 方案已废弃(2026-08-02):本文描述的构建期压缩插件已随图片迁移下线——图片现由独立图床仓库 AnAcretiondisk9986/blog-images + jsDelivr CDN 托管,文章/画廊直接引用外链,不再需要构建期压缩。本文保留作历史技术记录与踩坑参考,现役方案见文末附录

需求

站点图片全部位于 public/image/,文章正文直接以 Markdown 引用原图。部分截图高达 2–6MB,文章页加载明显变慢。目标是:

  • 文章页图片做「适当压缩」,加快加载;
  • 画廊页/gallery/)无论卡片还是大图预览,都不希望失去原图质量——画廊是图像档案,画质优先。

后经讨论进一步明确:画廊卡片缩略图也采用压缩版(滚动时快速呈现),点击查看大图时才加载原图。

方案概览

核心思路:构建期压缩 + 双钩子协作,不引入运行时处理,不改动文章 Markdown 内容。

  1. Sätteri hast 插件(Markdown 渲染时):把文章正文里的 /image/xxx 压缩为 WebP,src 替换为压缩版;
  2. Astro integrationastro:build:done):构建收尾时把压缩产物复制进 dist
  3. 画廊页:通过 optimizeImageUrl() 为卡片取压缩版 URL,data-gallery-src 仍保留原图供查看大图。

压缩参数:WebP quality: 78,宽度超过 1920px 才等比缩小(文章容器宽度远小于此,视觉无差别)。

public/image/xxx.png        原图(入库)
public/image/opt/<hash>-xxx.webp   构建产物(gitignore,不入库)
dist/image/opt/<hash>-xxx.webp     部署产物

技术难点与解决

1. Astro 7 默认换用 Sätteri Markdown 管道

Astro 7 的默认 Markdown 处理器从 unified(remark/rehype)换成 Sätteri。传统的:

markdown: { rehypePlugins: [myPlugin] }

直接报错,要求安装 legacy 包 @astrojs/markdown-remark。新写法:

import { satteri } from '@astrojs/markdown-satteri';

export default defineConfig({
  markdown: {
    processor: satteri({
      hastPlugins: [myHastPlugin],
    }),
  },
});

插件 API 也完全不同:不再是 unified 的 tree 遍历,而是按节点类型过滤的 visitor:

const plugin = {
  name: 'astro-optimize-images',
  element: {
    filter: ['img'],
    async visit(node, ctx) {
      // 只处理 <img>;ctx.setProperty() 原地改属性
    },
  },
};

2. 渲染阶段写 dist/ 会被清掉

最初尝试在 hast 插件里直接写 dist/image/opt/,构建后文件全部丢失——Astro 的构建流程中,路由渲染之后还有步骤会清理/重建 dist渲染阶段写入 dist 不可靠

解决:插件只把产物写进源目录 public/image/opt/(它会被 vite 复制进 dist,且渲染缓存命中时旧产物依然在),再由 astro:build:done 钩子在构建完全结束后统一复制到 dist/image/opt/

3. astro.config 与渲染管道是两份独立模块

astro.config.mjs 被 esbuild 打包成独立的 config 模块图,与 vite 渲染管道互不共享内存。模块级变量(如产物清单 Map)在两者之间不通——integration 读到的永远是空。

解决:不依赖内存状态,以文件系统为通道。插件写 public/image/opt/,integration 直接扫描该目录复制,天然解耦。

4. 渲染缓存:插件「根本没执行」也能出对的结果

最隐蔽的一个坑。Astro 7 会缓存 Markdown 编译结果:配置不变时,二次构建直接复用缓存的 HTML,hast 插件不会运行。此前多次「看起来成功」的构建,其实是首次运行留下的缓存。

这解释了为什么产物目录始终为空,但 HTML 里 src 却已被替换成 .webp

解决:产物放源目录 public/image/opt/(缓存命中时旧文件仍在),astro:build:done 无条件从源目录重建 dist/image/opt/,与「插件是否运行」彻底解耦。

实现细节

产物复用与失效

optimize() 对同一 src 有模块级缓存(防重复压缩),产物已存在且源图 mtime 未更新时直接复用;源图被替换后 mtime 变新,自动重新压缩。换图无需手动清缓存。

文件名防碰撞

输出文件名带目录 hash 前缀

const dirHash = createHash('sha1').update(path.dirname(decoded)).digest('hex').slice(0, 8);
const outName = `${dirHash}-${fileBase}.webp`;

避免不同子目录下同名文件互相覆盖。

安全与容错

  • 路径穿越防护:解码后的路径必须仍位于 public/ 内;
  • 仅处理 /image/ 前缀,其余路径直接跳过;
  • 单张压缩失败:console.warn + 回退原图,不中断构建
  • dev 模式完全不压缩,行为与改动前一致。

效果数据

原图 大小 压缩后 降幅
文章截图 PNG 2,032,852 B 63,088 B ≈97%
文章截图 PNG 2,142,891 B 92,284 B ≈96%
独立收藏 PNG 5,073,386 B 93,792 B ≈98%

文章页与画廊卡片加载的流量降为原来的 2–4%,画廊大图预览不受影响。

维护说明

本节指引随插件下线已失效,现役操作见文末附录「维护」小节。

  • 以后写文章:图片照旧放进 public/image/、Markdown 里正常引用,压缩自动生效
  • 新增独立收藏:只需写进 src/data/gallery.json,构建时自动生成卡片缩略图;
  • 想调压缩强度:改 src/plugins/optimize-images.mjs 里的 QUALITY / MAX_WIDTH
  • 回滚:git revert <commit> 即可,压缩产物不入库、不留残留。

结语

这次优化最大的成本不在压缩本身(sharp 一行事),而在于搞清楚 Astro 7 的新架构:Sätteri 管道、构建时序、模块隔离与渲染缓存。最终用「源目录产物 + 收尾复制」的简单架构绕开了全部四个坑,文章与画廊各取所需——正文快、画廊全。

附录:2026-08-02 方案演进——独立图床仓库 + jsDelivr CDN(现役)

为什么演进

  • 仓库膨胀:41MB 原图全部入库,.git 越来越大,影响 clone/备份,GitHub 也建议单仓库控制在 1GB 内;
  • 境内访问慢github.ioraw.githubusercontent.com 在国内直连速度差,图片加载是页面体验的最大瓶颈;
  • 机制冗余:构建期压缩 + 画廊双层图(压缩图/原图)在图片整体变小后失去意义。

新架构

  1. 独立公开图片仓库 AnAcretiondisk9986/blog-images:与博客仓库彻底分离,图片不再进入博客仓库;
  2. jsDelivr CDN 外链https://cdn.jsdelivr.net/gh/AnAcretiondisk9986/blog-images@main/image/<name>.webp——有国内节点,境内访问快,首次访问后全站 CDN 缓存;
  3. 一次性批量压缩:迁移时用 sharp 把 31 张原图统一转 WebP(参数沿用 quality 78、超 1920px 等比缩小),41MB → 2.9MB(平均 96KB/张);
  4. 管理面板自动上传:上传/URL 导入 → 自动转 WebP → 写入本地 ../blog-images/image/ → git 推送图片仓库 → 返回 jsDelivr 外链,写作流程不变(粘贴即用);
  5. 画廊:预览与点开大图均为同一份 WebP(图小,点开直接命中浏览器缓存);独立收藏另有 image/original/ 原图归档(6 张,18MB),查看器内「加载原图」按钮可切换原图/压缩版,并按图片长宽比自适应窗口、支持缩放滑块与拖拽平移;
  6. 构建期插件已删除src/plugins/optimize-images.mjs 下线,astro.config.mjs 不再注册,构建不再依赖 sharp(sharp 仅保留给管理面板上传转码)。

效果

  • 博客仓库图片体积 41MB → 0(图片仓库约 21MB,含原图归档);
  • 境内加载走 jsDelivr 国内节点,明显快于 github.io;
  • 图片不再占用 Pages 带宽,博客仓库 clone 更快。

维护

  • 写文章:管理面板粘贴图片即可,自动完成「压缩 → 推送图床 → 生成外链」;
  • 手动加图:文件放进本地 ../blog-images/image/,在该目录 git push origin main(注意图片仓库 remote 是 SSH,HTTPS 直连 GitHub 会被重置);
  • 更新同名文件:jsDelivr 有缓存,需访问 https://purge.jsdelivr.net 强制刷新;
  • 原图底片:独立收藏 6 张在 image/original/(从博客 git 历史恢复),其余原图仍留在博客仓库 git 历史中(9c99de8 之前的 commit 可恢复)。
读完了FINIS

文章到这里结束本条目至此结束

评论COMMENTS IN THE MARGIN— ITEMS
仅文字 · 最多 300 字 · 显示 IP 属地仅文字 · 不能附加图片 · 发送后标注 IP 属地0 / 300