sbt默认增量编译不够快,因其依赖文件时间戳和粗粒度依赖图,易误判隐式类、高阶类型推导等变更影响范围,且每次编译均有jvm启动开销;bloop通过常驻进程、semanticdb语义缓存及scala 3原生支持显著提升增量精度与速度。

为什么 sbt 默认增量编译还不够快
因为 sbt 的增量编译依赖源文件时间戳和粗粒度的依赖图,一旦涉及隐式类、高阶类型推导或跨模块 trait 继承,它容易误判“受影响范围”,触发不必要的重编。更关键的是,sbt 每次启动 compiler 实例都有 JVM 启动开销,对频繁保存-调试的开发流很不友好。
常见错误现象:sbt compile 即使只改一行 case class 字段,也卡在 Compiling 1 Scala source to .../classes... 超过 3 秒;sbt test 反复重建整个 test-classes 目录。
- Bloop 是常驻进程,跳过 JVM 冷启动,首次编译后所有后续编译都在已热加载的上下文中执行
- 它用语义数据库(SemanticDB)替代时间戳,能精确识别
def map[A, B](f: A => B): F[B]这类签名变更是否影响下游 - 对
given、export、transparent等 Scala 3 特性有原生支持,而 sbt 2.x 插件需额外桥接
如何验证 Bloop 是否真正接管了编译
不是装了插件就等于生效——Metals 可能仍在 fallback 到 sbt。最直接的判断方式是看终端输出和状态栏图标。
使用场景:你在 VSCode 中修改 src/main/scala/App.scala 并保存。
- 成功时:底部状态栏显示
Bloop: compiling myproject,且终端里看不到[info] Compiling 1 Scala source...这类 sbt 日志 - 失败时:状态栏仍显示
sbt shell或Importing build...,终端刷出大量[warn] Multiple main classes detected. - 检查命令:运行
bloop projects应列出你的项目名;bloop compile myproject应在 1 秒内返回Success!
build.sbt 中必须删掉的三类配置
Bloop 和 sbt 构建逻辑不同,某些对 sbt 有效的配置会干扰 Bloop 的依赖解析,导致增量失效或类型补全错乱。
参数差异:Bloop 不读取 scalacOptions in Compile 的部分值,也不支持 autoCompilerPlugins 自动激活。
- 删掉
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "...")—— Metals 已内置 Bloop 集成,重复引入会导致构建协议冲突 - 删掉
Compile / compile := (Compile / compile).dependsOn(bloopExport).value这类手动 hook,Bloop 通过.bloop/下的 JSON 配置驱动,不走 sbt task 链 - 删掉
scalacOptions += "-Ycache-plugin-class-loader:last-modified"—— Bloop 自带更细粒度的 classloader 缓存,该选项反而引发ClassCastException在隐式解析阶段
哪些代码改动仍会触发全量重编
Bloop 的增量能力很强,但不是万能。某些结构性变更无法被语义缓存覆盖,必须清空状态重新索引。
性能影响:这类操作平均增加 8–15 秒延迟,远超正常增量的 200–600ms。
- 修改
build.sbt中的scalaVersion或libraryDependencies—— Bloop 会检测到 project definition 变更,自动触发bloopClean - 新增或删除
src/main/scala/package.scala—— 它影响整个包的隐式作用域,Bloop 无法局部推导影响边界 - 在
trait中添加def并被 10+ 个子类实现 —— 当前 Bloop 0.4.12 对深度继承链的增量传播仍有保守策略,建议拆成sealed trait+enum改进
容易被忽略的地方:Bloop 的缓存目录 .bloop/ 默认不随 git clean -fdx 清除,长期迭代后可能堆积 stale semanticdb 文件,建议定期运行 bloopClean 或在 CI 中加入 rm -rf .bloop 步骤。











