Ruby 核心贡献者 byroot 近日披露,他在 Intercom 优化大型单体应用 CI 时,重新推动了一个拖了多年的底层能力:改进 Ruby 的目录扫描接口,减少 require 相关的路径查找开销。直接结果是,Bootsnap 在扫描约 3.2 万个文件、1 万个目录时,耗时从约 500 毫秒降到 230 毫秒,接近 2 倍提升。

这件事真正重要的地方,不是 Ruby 终于又快了“几点几倍”,而是它击中了一个被很多团队低估的成本中心:应用启动时间。对拥有 1350 个并行 CI worker 的 Intercom 来说,启动阶段每省 1 秒,整次构建就能少消耗 20 多分钟计算时间。反过来看,它也没那么重要——如果你维护的是一个中小型 Ruby 服务、CI 并行度不高,这次优化带来的体感,可能远没有一条慢 SQL 或一个臃肿测试工厂明显。

Bootsnap 优化的不是业务代码,而是 Ruby 的“上班打卡”

Bootsnap 早已是 Rails 默认依赖之一,2017 年就被引入 Rails 默认 Gemfile。它做的核心工作之一,是替 Ruby 缓存 $LOAD_PATH 中可加载文件的位置,绕开原生 require 那种近乎线性扫描目录、反复 stat 文件的老办法。

问题在于,Ruby 项目越大,这套机制越吃亏。原文给出的判断很直接:启动成本大致会随着 $LOAD_PATH.size$LOADED_FEATURES.size 一起膨胀,接近 O(N*M)。这也是为什么一个塞了 400 个 gem 的老 Rails 单体,启动速度往往不是 200 个 gem 项目的“两倍慢”,而是可能糟得多。Aaron Patterson 在 2015 年 GORUCO 演讲里就提过这个老问题,Shopify、Intercom 这类超大 Ruby 单体,过去十年一直在为它埋单。

真正的瓶颈,是目录扫描里的“N+1 系统调用”

这次优化的切口很工程化:Bootsnap 在递归扫描目录时,过去会对每个条目调用一次 File.directory?,背后通常就是一次 stat(2) 系统调用。对 Web 开发者来说,这就像把“N+1 查询”搬到了操作系统层。

在 Linux 和 BSD 里,底层 readdir(3) 其实早就能告诉程序“这个条目是不是目录”,不必再额外 stat 一遍。Ruby 内部某些方法已经利用了这个能力,但 Dir.foreach 这类常用接口并没有把信息暴露出来。byroot 早在 2020 年就为此提过 Dir.scan 的特性请求,当时几乎没推进;这次他先做了原型,又在 Bootsnap 自己的 C 扩展里落地,才把改进直接变成了可测量的收益。

这里有一个原文没展开、但很现实的约束:Bootsnap 不是随便扫一遍就行,它还得精确模拟 Ruby 的加载语义,不能因为“看起来不会有人把目录命名成 foo.rb”就偷懒。也因此,这类优化很难靠一行 Dir["*/.{rb,so}"] 替代。它考验的是语言运行时和生态工具之间的细密配合,而不是某个框架的表面技巧。

对谁最有用,谁可能几乎无感

这次变化的受益面并不平均。

对象直接收益现实体感
大型 Rails/monolith 团队CI 启动更快,算力成本下降最明显,尤其是高并行测试场景
使用 Bootsnap 的中型项目本地 boot 和测试准备略有改善有提升,但未必是首要瓶颈
小型 Ruby 服务或脚本几乎没有结构性变化大概率无感
Ruby 生态工具作者能写出更高效的目录遍历逻辑影响长期,短期不显眼

如果你是平台工程、基础设施或 Developer Experience 团队,这类改动最现实的后续动作通常不是“升级一下就完了”,而是会推动几件事:

  • 统一 Ruby 与 Bootsnap 版本
  • 重新测量 CI setup 时间占比
  • 判断是否继续加大并行度
  • 评估缓存策略是否还划算

这也是它和很多“跑分新闻”的区别:普通开发者未必立刻感觉更快,但负责预算和流水线效率的人,会很快看到报表变化。

Ruby 追赶的不是语法热度,而是工程效率底盘

把这件事放大看,它代表的是 Ruby 社区这两年的一个清晰方向:不只谈语法糖和新范式,而是持续修补运行时、解析器、YJIT、标准库 API 这些“底盘工程”。相比 Python 近年围绕启动速度、包管理和 C 扩展兼容性反复拉扯,Ruby 的优势之一恰恰是能在不大规模破坏应用的前提下,慢慢挤出这些历史包袱里的性能空间。

不过也别过度乐观。Ruby 核心团队最终没有直接修改现有 Dir 方法签名,而是倾向于新增 Dir.scan,原因就是兼容性风险。这说明语言级优化再合理,也得服从生态稳定性。另一层限制是,Bootsnap 的缓存失效仍受文件系统 mtime 和 Git checkout 行为影响,跨机器、跨构建重用缓存并不总是可靠。也就是说,目录扫描更快了,不等于启动问题就彻底解决了;大型 CI 的瓶颈,依旧可能落在数据库准备、依赖安装、容器拉取等更昂贵的环节。