fast_build_runner

pub package

Faster incremental rebuilds for Dart/Flutter projects built on top of build_runner.

fast_build_runner keeps builders, analyzer, and generated code in Dart. It currently focuses on speeding up the watch / incremental path while keeping generated outputs aligned with upstream.

Current best public signal:

  • up to 6.66x faster incremental rebuilds
  • generated Dart outputs verified to match upstream on a large real Flutter app
  • one-shot build stays on the upstream build_runner build path
  • the current optimization target is the watch / incremental workflow
  • the main Dart runtime / hot-path code lives in packages/fast_build_runner_internal/lib/src/

Start Fast

Fast test:

dart pub global activate fast_build_runner
fast_build_runner watch

Then start editing files for rebuilds.

This expects a Dart or Flutter project that already uses build_runner.

For one-shot builds, keep using the upstream build path:

fast_build_runner build --delete-conflicting-outputs

Table Of Contents

Current Headline

  • ✅ Up to 6.66x faster incremental rebuilds on a large real Flutter app
  • ✅ Custom bootstrap path with real upstream BuilderFactories
  • ✅ Custom child runtime around upstream BuildPlan / BuildSeries
  • ✅ Generated Dart outputs match upstream byte-for-byte on a large real Flutter app
  • ⚠️ Rust mode is still experimental

What This Project Is

fast_build_runner currently experiments with three layers:

  1. Custom bootstrap
    • Generates a custom entrypoint instead of handing everything to the stock ChildProcess.run(...).
  2. Custom watch runtime
    • Reuses upstream planning/build internals while controlling the watch loop, batching, and incremental scheduling.
  3. Alternative source engines
    • dart source engine: current safe default
    • rust source engine: optional experimental accelerator
    • upstream: baseline for comparison

What This Project Is Not

  • not a full rewrite of build_runner
  • not a full invalidation-engine replacement
  • not yet a production-ready replacement for every builder topology
  • not yet a solved story for workspaces / post-process / lazy phases

Why It Exists

The practical bottleneck for many teams is not code generation itself, but:

  • slow startup for repeated watch sessions
  • broad invalidation after a small edit
  • repeated rebuild scheduling on tiny changes
  • too much work happening outside the actual tracked builder actions

The current strategy is deliberately narrow:

  • keep the ecosystem-compatible parts in Dart
  • fork only the hottest internal paths when needed
  • measure everything on a real Flutter app instead of synthetic claims only

Current Default

The current recommended default is:

  • build proxies to upstream build_runner build
  • Dart source engine by default
  • Rust source engine only as an opt-in experimental mode
  • fast runtime is currently focused on watch / incremental workflows

If you do not pass --source-engine, fast_build_runner already uses dart.

Why this is the default:

  • it already shows strong incremental wins
  • it matches upstream generated Dart outputs
  • it avoids the current Rust startup penalty on short sessions

Real-World Results: Two Flutter Apps

Benchmarks below were run on two real Flutter apps with controlled mutation profiles.

Incremental Rebuild vs Upstream build_runner (Large Private App)

Scenario Upstream fast_build_runner (dart) fast_build_runner (rust)
JSON serializable model change 4.41s 0.97s (4.54x) 0.93s (4.76x)
Freezed model change 8.75s 5.02s (1.74x) 4.99s (1.75x)
DI-heavy registration change 25.10s 4.37s (5.75x) 3.77s (6.66x)

Incremental Rebuild vs Upstream build_runner (Second Real Flutter App)

Scenario Upstream fast_build_runner (dart) fast_build_runner (rust)
JSON serializable request model change 11.94s 7.01s (1.70x) 7.07s (1.69x)
Freezed request model change 9.93s 5.40s (1.84x) 5.24s (1.90x)
Typed route change 9.86s 4.61s (2.14x) 5.19s (1.90x)
Retrofit API contract change 11.05s 5.00s (2.21x) 6.14s (1.80x)

Current Interpretation

  • The main value today is incremental rebuild speed.
  • The Dart mode is the current safe public story and already wins on both real apps above.
  • The Rust mode already wins on some heavy cases, but still has bad total behavior on short DTO-style sessions because its startup cost does not always amortize.
  • The strongest result so far is the DI / injection-heavy case.

Correctness Status

The most important current correctness claim is:

  • upstream build_runner
  • fast_build_runner --source-engine=dart

produce the same generated Dart outputs on the large private Flutter app for the current regression scenario.

There is now a regression test for that in:

What is expected to match

Generated code outputs such as:

  • *.g.dart
  • *.freezed.dart
  • *.config.dart
  • *.gr.dart
  • *.mocks.dart

What may differ without being a correctness bug

Build metadata and cache/tooling artifacts, for example:

  • .flutter-plugins-dependencies
  • build/**/outputs.json
  • build/**/.filecache
  • build/**/gen_localizations.*

Those files are not the generated API/code outputs that users review and commit.

Quick Start

Run commands from this repository root.

Bootstrap seam proof

/Users/belief/dev/flutter/bin/dart run bin/fast_build_runner.dart spike-bootstrap

Long-lived watch loop on the default Dart engine

/Users/belief/dev/flutter/bin/dart run bin/fast_build_runner.dart watch

Finite watch-alpha proof on the Rust engine

/Users/belief/dev/flutter/bin/dart run bin/fast_build_runner.dart spike-watch \
  --source-engine=rust

One-shot build through the upstream path

/Users/belief/dev/flutter/bin/dart run bin/fast_build_runner.dart build \
  --delete-conflicting-outputs

This intentionally proxies to upstream build_runner build, so single builds keep upstream cold-start behavior while fast_build_runner stays focused on the watch / incremental path.

Compare engines

/Users/belief/dev/flutter/bin/dart run bin/fast_build_runner.dart benchmark-watch \
  --include-upstream \
  --output=summary

Real project benchmark on a local real app

/Users/belief/dev/flutter/bin/dart run bin/fast_build_runner.dart benchmark-watch \
  --fixture="$FAST_BUILD_RUNNER_REAL_APP_PATH" \
  --mutation-profile=profiles/real_app/analytics_service_injection.json \
  --include-upstream \
  --output=summary

Set FAST_BUILD_RUNNER_REAL_APP_PATH to your local Flutter app path first.

Example:

export FAST_BUILD_RUNNER_REAL_APP_PATH=/absolute/path/to/your/flutter_app
/Users/belief/dev/flutter/bin/dart run bin/fast_build_runner.dart benchmark-watch \
  --fixture="$FAST_BUILD_RUNNER_REAL_APP_PATH" \
  --mutation-profile=profiles/real_app/analytics_service_injection.json \
  --include-upstream \
  --output=summary

CLI Commands

  • build
    • proxies to upstream build_runner build for one-shot builds
  • watch
    • runs a long-lived fast watch loop for the current project
  • spike-bootstrap
    • proves the bootstrap seam with a generated custom entrypoint
  • spike-watch
    • runs one finite watch/incremental proof scenario with a chosen source engine
  • benchmark-watch
    • compares engines, optionally against upstream baseline

Architecture

target project
    |
    v
fast_build_runner CLI
    |
    v
custom generated entrypoint
    |
    v
child-side runtime
  - BuildPlan
  - BuildSeries
  - watch scheduler
  - custom perf probes
    |
    +--> Dart source engine
    |
    +--> Rust source engine

Upstream dependency pin

The current research pin is:

  • 2b1450e313a188a1027f04940e0e4e82372d6530

The local upstream source-of-truth clone lives in:

  • research/dart-build/

Main Technical Direction

The current most promising direction is not "rewrite everything in Rust".

The strongest signal so far is:

  • keep builders and analyzer in Dart
  • use a narrow Dart-side fork for hot internal paths
  • use Rust only where it genuinely helps as an optional source/watch engine

In practice that means the next strong wins are expected from:

  • analysis/resolver state retention between incremental builds
  • less repeated sync into the analyzer-facing filesystem
  • tighter watch/update ingestion

Main Constraints

  • analyzer-heavy builders are still bounded by upstream Dart-side costs
  • full build-script freshness parity is still narrower than stock upstream
  • Rust is currently a source-engine experiment, not a full graph daemon
  • some project profiles still show weak or negative Rust total wall-clock gain
  • workspace / post-process / lazy-phase coverage is not complete yet

Honest Public Positioning

Good:

  • "experimental companion for build_runner"
  • "real wins on a large private Flutter app"
  • "generated Dart outputs match upstream in the Dart mode"
  • "Rust mode is optional and still experimental"

Bad:

  • "universal build_runner replacement"
  • "Rust makes everything faster"
  • "all builder topologies are solved"

Repository Health Commands

Analyze:

/Users/belief/dev/flutter/bin/dart analyze

Targeted tests:

/Users/belief/dev/flutter/bin/dart test \
  test/bootstrap_spike_test.dart \
  test/real_app_output_compatibility_test.dart \
  test/watch_alpha_test.dart \
  test/watch_benchmark_test.dart \

Rust daemon tests:

cd native/daemon && cargo test

Project Documents

Libraries