跳到主要内容
返回墨迹
/陈墨· 修订于

用 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 │ ![构建火焰图](./flamegraph.png)
  │   ───────────────────────── 无障碍检查未通过

  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,欢迎围观。