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 的瓶颈,依旧可能落在数据库准备、依赖安装、容器拉取等更昂贵的环节。
