相信寫過 Flutter 的人對 build_runner 印象不太好,主要是過去它的效能較差、失敗率偏高,所以很多人能不使用就不使用。
但這些年過去後,build_runner 已經改變不少。特別是從 v2.7.0 到 v2.13.0,透過加入 AOT 編譯、大幅減少 I/O 與序列化開銷、以及優化程式碼分析流程等改進,現在版本的效能已有明顯提升。
特別是 2.13.0 版本,在官方的測試中,針對大型增量構建實現了最高 4 倍的加速。具體測試環境與指標為:

從測試結果可以看到:專案越大、提升越明顯。速度提升倍數如下:
另外,在所有規模下,增量構建(incr)的提升倍數普遍高於初始構建(init):

另外,當 build_runner 搭配下一代 Dart 分析器(analyzer)優化後的表現:
以 10000 庫為基準,差異如下:
| 版本階段 | 初始構建 (init) | 增量構建 (incr) | Web 增量構建 |
|---|---|---|---|
| v2.12 (舊版) | 48.75s | 45.72s | 55.24s |
| v2.13.0 (新版) | 16.12s (3.0x) | 11.80s (3.9x) | 18.11s (3.1x) |
| v2.13 + 新版分析器 | 12.48s (3.9x) | 9.46s (4.8x) | 15.63s (3.5x) |
而且這還只是 2.13 對比 2.12,實際上從 2.10 開始就已經有不少改善。
v2.10.0 - v2.12.0 的一些重要性能改善包括:
因此直到 2.13,這些修改帶來的提升主要涉及以下幾點:
不需要序列化的資源圖複製
在 watch 或 serve 模式下,每次構建之間需要重置資源圖(Asset Graph)。舊版本需要將圖序列化到磁碟再讀回,造成大量 I/O 開銷。而 2.13 在 graph.dart 中引入了 copyForNextBuild 方法,直接在記憶體中複製節點樹,在頻繁儲存程式碼觸發的增量構建場景非常實用,例如:
AssetGraph copyForNextBuild(BuildPhases buildPhases) {
return AssetGraph._with(
nodes: _nodes.clone(), // 直接複製記憶體中的節點,避免序列化
// ... 複製其他元資料
previousInBuildPhasesOptionsDigests: inBuildPhasesOptionsDigests,
inBuildPhasesOptionsDigests: buildPhases.inBuildPhasesOptionsDigests,
// ...
);
}
復用語法錯誤計算結果
Resolver 在處理 library 時需要檢查語法錯誤。透過復用 analyzer 已有的解析結果,可以避免重複計算,例如:
Future<List<AnalysisResultWithDiagnostics>> _syntacticErrorsFor(
LibraryElement element,
) async {
final parsedLibrary = _driver.currentSession.getParsedLibraryByElement(element);
if (parsedLibrary is! ParsedLibraryResult) return const [];
final relevantResults = <AnalysisResultWithDiagnostics>[];
for (final unit in parsedLibrary.units) {
if (unit.diagnostics.any((error) => error.diagnosticCode.type == DiagnosticType.SYNTACTIC_ERROR)) {
relevantResults.add(unit);
}
}
return relevantResults; // 利用 Analyzer 會話中的快取結果
}
復用 Trigger 配置摘要
Builder 可以配置 triggers(觸發器),只有滿足特定條件(例如存在特定註解或 import)時才會執行。v2.13.0 開始快取這些配置的摘要(digest),避免每次都重複解析:
/// 只有當配置發生變化時,摘要才會改變
late final Digest digest = md5.convert(utf8.encode(triggers.toString()));
triggers 並不是 2.13 新增的機制,但到了 2.13 它的普及度與作用已經相當顯著。
Trigger 可以展開來說明:在過去的 Builder 通常會掃描專案中的所有 .dart 檔案,而透過觸發器(triggers),Builder 可以宣告:「只有當檔案包含特定的 import 或特定的 annotation(註解)時才會執行。」換句話說:
例如你開發或適配一個 my_generator 套件,並提供 my_builder 生成器,你可以修改 build.yaml,在 builders 定義中開啟 run_only_if_triggered,並在頂層加入 triggers 配置塊:
# 1. 在生成器定義中開啟觸發模式
builders:
my_builder:
import: "package:my_generator/builder.dart"
builder_factories: ["myBuilderFactory"]
build_extensions: {".dart": [".g.dart"]}
auto_apply: dependents
# 核心配置:宣告 Builder 僅在被觸發時執行
defaults:
options:
run_only_if_triggered: true
# 2. 定義具體的觸發規則(頂層設定)
triggers:
# 格式為 "套件名:生成器名"
my_generator:my_builder:
# 觸發條件1:檔案匯入了特定的庫(只需寫庫路徑,系統會自動加上 package: 前綴)
- "import my_generator/annotations.dart"
# 觸發條件2:檔案使用了特定的註解名稱
- "annotation MyCustomAnnotation"
目前支援以下兩種觸發器:
例如 built_value 就採用了類似的適配:只有在檢測到 import 'package:built_value/built_value.dart' 或 @SerializersFor 註解時才會運作。對於無關的原始檔案,build_runner 會在日誌中顯示為 "not triggered" 或 "skipped":

總結到 2.13 版本,主要透過三大方向達成跳躍式效能提升:
不過,這並不代表你什麼都不用做就能享受所有改善,例如:
那麼,你覺得 build_runner 對你有用嗎?
https://github.com/dart-lang/build/blob/master/build_runner/CHANGELOG.md