用 Rust 重写博客构建器:一次 47 倍的提速
把一个跑了三年的 Node.js 构建器扔进熔炉,用 Rust 重铸。本文记录架构取舍、坑与收益——从 14.2 秒到 0.3 秒的完整旅程。
缘起
三年前我给这个博客写了个 Node.js 构建器,当时全站构建只要两秒,我甚是得意。三年后,文章涨到两百篇、图片上万张,冷构建飙到了 14.2 秒。每次改一个错字都要等一轮咖啡凉的工夫,是可忍孰不可忍。
先说结论:用 Rust 重写后,冷构建 0.3 秒,增量构建 31 毫秒。这篇文章不讲 Rust 语法,讲的是重写一个「能用」的工具时,真正值得care的东西。
先测量,再动手
重写之前,我先用 hyperfine 和 flamegraph 采样,发现耗时分布大概是这样的:
| 阶段 | 耗时占比 | 主要开销 |
|---|---|---|
| Markdown 解析 | 18% | 重复解析未变更文件 |
| 图片处理 | 61% | sharp 调用序列化排队 |
| 模板渲染 | 12% | 字符串拼接 |
| 文件 IO | 9% | 逐个 await,毫无并行 |
有意思的是,61% 的开销来自图片,而其中八成图片根本没变过。所以重写的第一要务不是「换语言」,而是把内容寻址缓存做对。
架构上的三个决定
一、内容寻址,而不是路径寻址
旧构建器以文件路径为缓存键,改了任何模板就会全量失效。新构建器以「输入的哈希」为键:文件内容、前置配置、模板版本共同参与散列。模板改了,只有受影响的页面重新渲染。
struct ArtifactKey {
content: Sha256, // 文件内容
frontmatter: Sha256,
template: u32, // 模板版本号
}
二、并行是默认,串行是例外
Rust 的 rayon 让数据并行变得近乎免费。图片解码、缩放、编码全部扔进工作池,按 CPU 核数自动分摊。Node.js 时代我要小心翼翼地用 Promise.all 手工分组,还得提防事件池饿死——在 Rust 里这些焦虑不存在。
三、错误信息是产品的一部分
旧构建器的报错是 Error: ENOENT。新的报错长这样:
error[img-0231]: 图片缺少 ALT 描述
┌─ content/posts/rust-builder.md:47:3
│
47 │ 
│ ───────────────────────── 无障碍检查未通过
│
hint: 补充 alt 文本,或显式标记 `` 跳过检查
工具的用武之地在出错的那一刻。报错写得越清楚,用户越不需要读文档。
值不值
| Node.js 旧版 | Rust 新版 | |
|---|---|---|
| 冷构建 | 14.2 s | 0.31 s |
| 增量构建 | 1.8 s | 31 ms |
| 代码行数 | 2,100 | 2,700 |
| 心智负担 | 低 | 中 |
提速 47 倍当然爽,但诚实地说:其中大约 20 倍来自缓存设计而非语言本身。Rust 带来的是另外两样东西——把并发 bug 从「偶现」变成「编译期报错」,以及一种把数据结构想清楚再动手的纪律。
如果你也在犹豫要不要重写,我的建议是:先测量,找到那 61%;然后问自己,重写能否顺便还清技术债。语言之争是次要的,缓存键的设计才是构建器的灵魂。
附:构建器叫
omo(墨的日语读音),开源在 GitHub,欢迎围观。