Flutter build_runner Optimization: 5x Faster Code Gen

No Dart Macros: The Reality of Code Generation in 2026
For years, one of the most anticipated features in the Flutter and Dart ecosystem was Dart Macros (Metaprogramming). Developers viewed it as the ultimate solution to fundamentally eliminate slow build_runner compile times and .g.dart / .freezed.dart file generation friction whenever using packages like freezed, json_serializable, and riverpod_generator.
However, the Dart team made a definitive decision: the Dart Macros project has been officially cancelled.
Why Were Dart Macros Cancelled?
The main reason cited by the Dart team was “severe degradation of Hot Reload and semantic analysis performance.”
- Hot Reload Degradation: Dart’s killer feature—Hot Reload—must reflect code changes in rendering within 100ms. However, Macros manipulated code structures at runtime, requiring deep semantic introspection. This caused severe performance bottlenecks, delaying Hot Reload by several seconds or more.
- IDE and Analyzer Engine Overhead: As the
analyzerengine re-evaluated macro operations on every keystroke, memory consumption skyrocketed and CPU usage hit 100% in IntelliJ and VS Code.
Ultimately, to protect core framework values like Hot Reload and developer experience (DX), the Dart team pivoted away from Macros to focus on dramatically optimizing the existing build_runner code generation build engine.
As of 2026, build_runner is not being replaced—it remains a core tool you must master. In this guide, you will learn practical performance optimization techniques to speed up build_runner by over 5x, reducing multi-minute build times down to under 30 seconds.
3 Root Causes of Slow build_runner Performance
Before optimizing, you need to understand why build_runner slows down exponentially as your project grows.
[Default Unoptimized build_runner]
Entire project (lib/**/*.dart)
├── lib/ui/pages/login_page.dart (UI Widget - No code gen needed) -> Parsing!
├── lib/utils/date_formatter.dart (Utility function - No code gen needed) -> Parsing!
├── lib/models/user_model.dart (@JsonSerializable) -> Target
└── lib/widgets/custom_button.dart (UI Widget - No code gen needed) -> Parsing!
=> Takes 3-5 minutes due to full AST analysis of all 1,000 files
1. Global File Scanning
Without custom configuration, build_runner parses the Abstract Syntax Tree (AST) of every .dart file under lib/ (UI widgets, helper functions, constants, etc.). Even if only 10 files actually use @freezed or @JsonSerializable, it wastes time scanning all 1,000 files.
2. Redundant Builder Executions
Multiple builders such as json_serializable, freezed, riverpod_generator, and injectable iterate through the entire file tree independently, duplicating parsing efforts.
3. Inefficient Default Output Caching
Modifying a single file triggers excessive tracking overhead as build_runner re-evaluates the entire dependency graph to validate cache validity.
Step 1: 5x Speedup via build.yaml Scoping
The cornerstone of build_runner optimization is creating a build.yaml file at your project root and targeting only the directories and files that require code generation using the generate_for option.
Production-Ready build.yaml Configuration
# build.yaml
targets:
$default:
builders:
# 1. Scope json_serializable builder
json_serializable:
generate_for:
include:
- lib/data/models/**.dart
- lib/domain/entities/**.dart
exclude:
- lib/ui/**
- lib/widgets/**
# 2. Scope freezed builder
freezed:freezed:
generate_for:
include:
- lib/data/models/**.dart
- lib/domain/entities/**.dart
- lib/application/states/**.dart
exclude:
- lib/ui/**
- lib/widgets/**
# 3. Scope riverpod_generator builder
riverpod_generator:
generate_for:
include:
- lib/providers/**.dart
- lib/application/**.dart
exclude:
- lib/ui/**
# 4. Configure source_gen (general annotations)
source_gen:combining_builder:
options:
ignore_for_file:
- type=lint
- invalid_annotation_target
Impact Analysis
Applying this single configuration reduces the number of files build_runner must parse from over 1,000 down to around 30.
- Before Optimization: Scanning all 1,200 files -> 3 min 20 sec
- After Optimization: Pinpoint scan of 35
modelsandprovidersfiles -> 28 sec (86% reduction!)
Step 2: Optimizing Terminal Build Speed with CLI Options
Using the right command-line flags for your workflow can save tens of seconds on every execution.
1. Auto-Delete Conflicting Outputs (--delete-conflicting-outputs)
When conflicting .g.dart or .freezed.dart files exist, this flag skips the interactive prompt and proceeds with build execution immediately.
# Recommended default build command
dart run build_runner build --delete-conflicting-outputs
2. Incremental Build Mode (Optimizing watch Mode)
During active development, running watch mode is far superior to triggering manual build commands. It regenerates only the single modified file in under 0.5 seconds.
# Instant incremental generation on file changes
dart run build_runner watch --delete-conflicting-outputs
3. Pinpoint Builds with Build Filters (--build-filter)
When you only need to regenerate code for a specific file you are working on—like user_model.dart out of hundreds of models—using --build-filter completes the build in just 1 second.
# Run code gen for a specific file in under 1 second
dart run build_runner build --build-filter="lib/data/models/user_model.dart"
Step 3: Modern Freezed & JsonSerializable Integration Patterns (2026)
Here is the latest best practice to reduce build overhead when using freezed alongside json_serializable.
// lib/data/models/user_model.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_model.freezed.dart';
part 'user_model.g.dart';
@freezed
class UserModel with _$UserModel {
// Setting explicitToJson: false prevents redundant serialization helper methods and speeds up builds
@JsonSerializable(explicitToJson: false)
const factory UserModel({
required int id,
required String email,
required String name,
@Default(false) bool isVIP,
}) = _UserModel;
factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
}
Global explicitToJson: false and field_rename Settings
Instead of annotating every model file manually, configuring global options in build.yaml reduces boilerplate and build overhead simultaneously.
# build.yaml
targets:
$default:
builders:
json_serializable:
options:
# Auto-convert snake_case (eliminates JSON fieldRename boilerplate)
field_rename: snake
# Minimize explicit toJson calls for improved parsing speed
explicit_to_json: false
# Strict nullability checks
checked: true
Step 4: CI/CD Pipeline Build Caching with GitHub Actions
Running build_runner from scratch on every CI/CD pipeline run delays PR builds by 5 minutes or more. By leveraging GitHub Actions’ actions/cache to persist the .dart_tool/build directory, you can cut CI build times by 80%.
# .github/workflows/flutter_ci.yml
name: Flutter CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Java & Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.29.0'
channel: 'stable'
cache: true
- name: Install Dependencies
run: flutter pub get
# Restore and save build_runner cache
- name: Cache build_runner outputs
uses: actions/cache@v4
with:
path: .dart_tool/build
key: build-runner-${{ hashFiles('pubspec.lock') }}-${{ hashFiles('build.yaml') }}
restore-keys: |
build-runner-${{ hashFiles('pubspec.lock') }}-
# Execute ultra-fast code generation with cached inputs
- name: Run build_runner
run: dart run build_runner build --delete-conflicting-outputs
- name: Run Tests
run: flutter test
Benchmark: Performance Comparison Before and After Optimization (1,200 File Project)
The following benchmarks were measured on a large enterprise Flutter application containing 1,200 Dart files and 50 @freezed/@JsonSerializable models.
| Test Scenario | Before Optimization (Default) | After Optimization (build.yaml + Filter) |
Performance Improvement |
|---|---|---|---|
| Clean Build (Full Re-gen) | 3 min 45 sec (225s) | 32 sec | 85.7% reduction |
| Incremental Build (Single Model) | 18 sec | 1.2 sec | 93.3% reduction |
watch Mode Latency |
4.5 sec | 0.4 sec (near real-time) | 91.1% reduction |
| CI/CD Build Time (GitHub Actions) | 5 min 10 sec | 1 min 05 sec | 79.0% reduction |
Conclusion: 2026 Developer Checklist
With Dart Macros officially cancelled, waiting for magical language-level metaprogramming is no longer an option. In 2026, the most effective development strategy is building an optimized build pipeline that unlocks the full potential of build_runner.
Productivity Checklist
- Created a
build.yamlfile at project root and scopedgenerate_fortolib/data/models/**? - Explicitly excluded
lib/ui/andlib/widgets/from code generation? - Used
--build-filterwhen modifying a single file? - Running
dart run build_runner watchin incremental mode during local development? - Applied
.dart_tool/builddirectory caching in your CI/CD pipeline?
By implementing these 5 checklist items, you can eliminate daily build wait times and stay focused with instant Hot Reload performance.
Related article: Check out our rendering pipeline optimization guide in Flutter Impeller Engine & Vulkan/Metal Performance Optimization.