Dart — Architecture, Compilation Pipeline & Toolchain
Compilation Pipeline Internals
Dart has three execution modes. Understanding the boundary between them is critical for debugging performance and behavioral differences across platforms.
dart
// ── kernel snapshot (AST serialized, no machine code) ──
// Used by VM JIT: fast startup, interpreted or JIT-compiled at runtime.
// Pipeline: source → dart:_kernel (front-end) → .dill file
//
// ── AOT snapshot (precompiled to machine code) ──
// Used by Flutter release: no compiler at runtime, fast startup.
// Pipeline: source → kernel → TFA (tree-shaking) → machine code snapshot
//
// ── JIT (development) ──
// Source changes hot-reloaded via incremental kernel compilation.
// The VM compiles hot functions with an optimizing compiler (OSR).
// This program behaves differently in JIT vs AOT:
void main() {
// assert() runs in JIT (debug), stripped in AOT (release).
// In release, the entire assertion body is dead-code-eliminated.
assert(() {
print('This print vanishes in AOT — no side effect in release.');
return true;
}());
// In JIT, `DateTime.now()` in const context is rejected at compile time.
// In AOT, tree-shaking removes unused const symbols entirely.
print('Running in ${const String.fromEnvironment('dart.vm.product', defaultValue: false) ? 'AOT' : 'JIT'} mode.');
}
SDK Layout & Platform Conditionals
dart
// Platform detection at compile time (not runtime) — the compiler
// resolves these to `true`/`false` and dead-code-eliminates the loser.
const bool isWeb = bool.fromEnvironment('dart.library.html');
const bool isIO = bool.fromEnvironment('dart.library.io');
const bool isVM = !isWeb; // VM includes io
// Anti-pattern: runtime checks for platform in hot paths.
// ✓ Correct: compile-time conditional imports (zero runtime cost).
// platform.dart — public API
export 'platform_io.dart' // native: dart:io available
if (dart.library.html) 'platform_web.dart'; // web: dart:html available
dart
// platform_io.dart — compiled only on native (VM/AOT)
import 'dart:io';
String get platformName => Platform.operatingSystem; // 'macos', 'linux', etc.
int get cpuCount => Platform.numberOfProcessors;
// File system access, Process.run, Platform.exit — all available.
Future<List<int>> readBytesNative(String path) => File(path).readAsBytes();
dart
// platform_web.dart — compiled only on web (dart2js / DDC / Wasm)
// No dart:io. Use dart:html or fetch API.
import 'dart:html';
String get platformName => 'web';
int get cpuCount => 1; // web is single-threaded; use Web Workers for parallelism
// No File API — use HttpRequest / fetch for remote resources.
Future<List<int>> readBytesWeb(String url) async {
final response = await HttpRequest.request(url, responseType: 'arraybuffer');
return (response.response as ByteBuffer).asUint8List();
}
pubspec.yaml — Production Configuration
yaml
# pubspec.yaml — the single source of truth for the package graph.
name: my_service
description: A production Dart microservice.
version: 1.2.0
publish_to: none # set to 'none' for private apps; omit to publish to pub.dev
environment:
sdk: '>=3.4.0 <4.0.0' # SDK constraint — using Dart 3.4+ features requires this floor
dependencies:
# ^x.y.z = >=x.y.z <(x+1).0.0 — allows minor/patch, blocks breaking major.
http: ^1.2.0
shelf: ^1.4.0
shelf_router: ^1.1.0
# Git dependency — for forks or unreleased versions.
# my_lib:
# git:
# url: https://github.com/org/my_lib.git
# ref: dev-branch # or a commit SHA or tag
# Path dependency — for monorepo local packages.
shared_models:
path: ../shared_models
dev_dependencies:
test: ^1.25.0
mocktail: ^1.0.0
lints: ^4.0.0
build_runner: ^2.4.0
Version Resolution: pubspec.lock
bash
# pubspec.yaml declares constraints; pubspec.lock pins exact versions.
# Apps: commit pubspec.lock (reproducible builds).
# Libraries: omit pubspec.lock (let the consumer resolve).
dart pub get # resolve + download → writes pubspec.lock
dart pub upgrade # re-resolve within constraints → updates lock
dart pub outdated # show which deps have newer versions available
Native Executable Compilation (AOT)
bash
# Compile to a standalone native binary — no Dart SDK needed at runtime.
# Tree-shaken, AOT-compiled, fast cold start. Perfect for Docker / CLI tools.
dart compile exe bin/server.dart -o bin/server
# The binary is platform-specific (macOS binary won't run on Linux).
# Cross-compile via Docker:
# docker run --rm -v "$PWD":/app -w /app dart:stable dart compile exe bin/server.dart -o bin/server
# Kernel snapshot (not native — needs VM, but faster than source):
dart compile kernel bin/server.dart -o bin/server.dill
# JIT snapshot (precompiled classes, still needs VM):
dart compile jit-snapshot bin/server.dart -o bin/server.jit
Project Structure — Production Layout
text
my_service/
├── bin/
│ └── server.dart # entry point — thin, delegates to lib/
├── lib/
│ ├── my_service.dart # public API barrel (exports only public code)
│ ├── src/ # convention-private — users should NOT import
│ │ ├── server.dart # server bootstrap
│ │ ├── handlers.dart # request handlers
│ │ ├── middleware.dart # auth, logging, CORS
│ │ └── models.dart # internal models
│ └── my_service.dart # re-exports public API
├── test/
│ ├── unit/
│ │ └── handlers_test.dart
│ └── integration/
│ └── server_test.dart
├── analysis_options.yaml # linter rules + analyzer config
├── pubspec.yaml
├── pubspec.lock # commit for apps
├── Dockerfile
└── Makefile
dart
// bin/server.dart — thin entry point, all logic in lib/
import 'package:my_service/my_service.dart';
void main(List<String> args) async {
await runServer(args); // delegate to lib/src/server.dart
}
analysis_options.yaml — Enforcing Standards
yaml
include: package:lints/strict.yaml # strictest rule set
analyzer:
exclude:
- 'build/**'
- '**/*.g.dart' # generated files
- '**/*.freezed.dart' # generated files
language:
strict-casts: true # no implicit casts (num → int requires explicit `as`)
strict-inference: true # flags `var x;` (no initializer, infers dynamic)
strict-raw-types: true # flags `List` without type args
linter:
rules:
- avoid_print: false # allow print in CLI tools
- prefer_const_constructors
- prefer_final_locals
- prefer_final_in_for_each
- require_trailing_commas
- unawaited_futures # flags `future();` without await/unawaited()
- cancel_subscriptions # flags StreamSubscription not cancelled
💡 Tips & Tricks
- Performance:
dart compile exeproduces a tree-shaken AOT binary — no compiler at runtime, ~50ms cold start. Use for Docker, CLI tools, and serverless. The binary includes the Dart runtime but not the compiler. - Debug:
dart run --observe bin/server.dartstarts the VM service (debugger protocol, CPU profiling). Connect VS Code or DevTools.--observe=8181pins the port. - Idiom:
const String.fromEnvironment('dart.vm.product')is a compile-time constant — the compiler resolves it and dead-code-eliminates the unused branch. Use it (not runtime checks) for platform/mode conditionals. - Idiom:
publish_to: nonein pubspec.yaml for private apps — prevents accidentaldart pub publishto pub.dev. Remove only when publishing a library. - Tooling:
dart pub deps --style=compactshows the dependency tree without long paths. Useful for auditing transitive dependencies.
⚠️ Edge Cases & Gotchas
assertis stripped in AOT: the entire assertion expression (including side effects) vanishes in release builds. Never put business logic or side effects insideassert(() { ... }()).inton web is a JS double: on native,intis 64-bit. On web (dart2js/DDC),intis a JS number (IEEE 754 double) — values above 2^53 lose precision. UseBigIntfor arbitrary-precision integers on web.dart:iois native-only: importingdart:ioin a web project fails at compile time. Use conditional imports (if (dart.library.html)).- No
dart:mirrorson web / AOT: runtime reflection is unsupported in dart2js and AOT. Use code generation (json_serializable,freezed) instead. dart runvsdart file.dart:dart runresolves the package and runsbin/<package_name>.dart.dart file.dartcompiles the file standalone (no package resolution for relative imports).pubspec.lockfor libraries: if you commitpubspec.lockfor a library, it can conflict with the consumer's resolution. Omit it for published libraries; commit it for apps.- Hot reload doesn't re-run
initState: in Flutter, hot reload preserves state (includingStateobjects). Changes toinitStatelogic require a hot restart (R). - SDK constraint floor:
sdk: '>=3.4.0 <4.0.0'— using a Dart 3.4 feature (e.g., pattern guards) in a project withsdk: '>=3.0.0'fails at analysis. Bump the floor to match feature usage.
🧠 Spot the Bug
A developer ships a server with an assert guarding a critical side effect. In production, the side effect never runs:
dart
void processOrder(Order order) {
assert(_sendNotification(order), 'Notification failed');
_commitOrder(order);
}
What happens in the AOT release build?
Answer
In AOT (release), assert is completely stripped — the expression _sendNotification(order) is never evaluated. The notification is silently skipped. Only _commitOrder runs.
The fix — use an explicit check, not assert:
void processOrder(Order order) {
if (!_sendNotification(order)) {
throw StateError('Notification failed for order ${order.id}');
}
_commitOrder(order);
}
assert is for development-time invariants (conditions that should never fail if the code is correct). Runtime validation (especially side effects) must use if/throw. The assert body is dead code in release.