| |

Dart 96 🎯 Internal Architecture: Dart Web Compilers (dart2js vs dart2wasm) Architecture

⏱️ Estimated read: 12 min | |

Dart targets the web through two distinct compilation pipelines that share a common frontend but diverge at the back end. dart2js translates Dart into JavaScript, using an SSA-based intermediate representation and a closed-world assumption to perform whole-program optimizations before emitting JavaScript that browsers can execute directly. dart2wasm translates Dart into WebAssembly, targeting the WasmGC proposal so that Dart’s garbage-collected object model maps onto Wasm’s own GC support rather than requiring a bundled collector. The two compilers are not interchangeable: dart2wasm requires a browser with WasmGC support and produces a .wasm module plus a JavaScript loader, while dart2js produces a single .js file that runs anywhere JavaScript runs.

Key point: dart2js and dart2wasm share the same frontend—the Dart analyzer and kernel IR—but use different backends. dart2js optimizes through SSA and emits JavaScript. dart2wasm lowers Dart objects directly to WasmGC structures and emits a binary module with a thin JavaScript instantiation layer.


Why two web compilers exist

The JavaScript compatibility problem. JavaScript is the only language that every browser executes natively. dart2js exists because it guarantees that a Dart web application runs everywhere, regardless of browser version or Wasm support. The compiler was designed from the start to produce JavaScript that is both semantically correct and highly optimized, using tree shaking to remove unused code and type inference to generate efficient output . The trade-off is that JavaScript’s dynamic nature imposes overhead that a lower-level target could avoid.

The WasmGC opportunity. WebAssembly was originally designed for languages without garbage collection, which meant Dart would have had to bundle its own collector into the module—an approach that would inflate code size and hurt startup time . The WasmGC proposal changed that by adding native support for garbage-collected languages. dart2wasm exists to take advantage of this: it compiles Dart’s object model directly to WasmGC structures, which promises faster startup and more predictable performance than JavaScript’s JIT path .

The feature detection problem. Because WasmGC support is not universal, a production deployment cannot assume that every browser can run a .wasm module. The build tooling solves this by generating an entrypoint loader that detects WasmGC support and loads either the Wasm module or the JavaScript fallback depending on the result . A dart2wasm-only build produces a .wasm file and a .mjs helper, but no <script>-ready entrypoint—the loader is what makes it usable in a browser .


a. dart2js: SSA Backend and JavaScript Emission

The dart2js pipeline begins with the Dart frontend, which parses source, resolves types, and produces Kernel IR. The backend then lowers this IR into an SSA (Static Single Assignment) form, which is the internal representation used for optimization . In SSA form, every variable is assigned exactly once, which makes data-flow analysis and optimization passes simpler and more effective. The compiler performs type inference, tree shaking, and minification on this representation before generating JavaScript .

The compiler’s architecture is organized around a “world” model: it assumes a closed world where the set of reachable classes and methods is known from the entry point. This assumption enables tree shaking—unreachable code is never included in the output. The enqueuer is the work-queue that tracks which elements are needed as compilation proceeds, growing the world from main outward . The output is a single JavaScript file (or a set of files with deferred loading) that runs in any JavaScript environment.

The emitter model is distinct from the input model. The compiler uses an entity model to describe Dart elements and a separate JS model to describe the concepts specific to JavaScript code generation, such as closure classes and constructor bodies . This separation allows the compiler to transform Dart semantics into JavaScript idioms without conflating the two representations.


b. dart2wasm: WasmGC Lowering and Module Emission

dart2wasm also starts from Kernel IR but targets WebAssembly rather than JavaScript. The key architectural decision is the reliance on WasmGC: Dart objects, classes, and arrays are represented as WasmGC structures, so the Wasm runtime’s garbage collector manages Dart objects directly. This avoids the complexity and overhead of bundling a Dart-specific GC into the module . The compiler lowers Dart’s type system onto Wasm’s type system, with some compromises: Dart null and JavaScript undefined are both internalized as Dart null in dart2wasm, whereas the JavaScript backends preserve the distinction .

The output of dart2wasm is not a standalone HTML-ready script. It produces a .wasm binary module and a .mjs JavaScript module that exports functions for instantiating the module . The build tooling generates an entrypoint loader that performs feature detection and loads the appropriate output . For non-browser runtimes such as Wasmtime, the --standalone flag produces a module without the JavaScript companion, but dart:js_interop is disallowed in that mode .

The compiler supports SIMD through Wasm’s 128-bit v128 instructions, and dart.library.isolate evaluates to false in dart2wasm, reflecting the absence of isolate support in the Wasm target . Packages that import dart:html or dart:js are not Wasm-compatible; the wasm-ready filter on pub.dev identifies packages that use only the supported interop mechanism .


c. Shared Frontend and Divergent Backends

Both compilers use the same Dart frontend: the analyzer, resolver, and kernel IR. This means that a program that type-checks for one compiler type-checks for the other, and the semantic differences are in the runtime behavior rather than the source-level correctness. The frontend is where Dart’s language features are desugared into a common representation, and the backends are where those features are mapped onto the target platform’s capabilities.

The divergence is in the runtime model. dart2js emits JavaScript that relies on the browser’s JavaScript engine for execution, garbage collection, and JIT optimization. dart2wasm emits Wasm that relies on the browser’s Wasm runtime for execution and WasmGC for garbage collection. The performance characteristics differ because the underlying runtimes differ: JavaScript engines are highly optimized for dynamic code, while Wasm engines are optimized for statically typed, low-level code. Initial benchmarks from the prototype stage showed faster time to first frame and faster average frame time for dart2wasm , though the exact numbers depend on the application and the browser.

The build tooling abstracts the choice. build_web_compilers can invoke dartdevc for development, dart2js for release, and optionally dart2wasm alongside dart2js for a dual build that falls back to JavaScript when WasmGC is unavailable . The entrypoint loader is the mechanism that makes the dual build transparent to the user: the browser either loads the Wasm module or the JavaScript file, and the application code does not need to know which one ran.


Complete Example Session

# ============================================
# PART 1: COMPILE WITH DART2JS
# ============================================
# dart2js is invoked through the build system or directly
dart compile js -O2 bin/main.dart
# Output: main.dart.js (single JavaScript file)


# ============================================
# PART 2: DART2JS SSA OPTIMIZATION PIPELINE
# ============================================
# Source → Kernel IR → SSA → Optimizations → JavaScript
# Optimizations include: tree shaking, type inference, minification


# ============================================
# PART 3: COMPILE WITH DART2WASM
# ============================================
dart compile wasm bin/main.dart
# Output: main.wasm (binary module) + main.mjs (loader helper)


# ============================================
# PART 4: DART2WASM WASMGC MAPPING
# ============================================
# Dart objects → WasmGC structs
# Dart arrays → WasmGC arrays
# Dart GC → WasmGC garbage collector
# No bundled collector; relies on Wasm runtime


# ============================================
# PART 5: DUAL BUILD WITH FEATURE DETECTION
# ============================================
# build_web_compilers generates an entrypoint loader
# The loader checks for WasmGC support
# If supported: loads .wasm via .mjs helper
# If not: loads .dart2js.js fallback


# ============================================
# PART 6: DART2WASM STANDALONE MODULE
# ============================================
dart compile wasm --standalone bin/main.dart
# Output: main.wasm (no .mjs companion)
# dart:js_interop disallowed in this mode


# ============================================
# PART 7: DART2WASM DEFERRED LOADING (EXPERIMENTAL)
# ============================================
dart compile wasm --enable-deferred-loading bin/main.dart
# Splits output into multiple modules loaded on demand


# ============================================
# PART 8: WASMGC BROWSER REQUIREMENTS
# ============================================
# Chrome/Edge 137+
# Safari 26.2+
# Firefox 134+ (147+ for Flutter web)
# Without these, the JavaScript fallback is required


# ============================================
# PART 9: NULL SEMANTICS DIFFERENCE
# ============================================
# dart2js: Dart null can map to JS null or undefined
# dart2wasm: Both JS null and undefined internalize as Dart null
# Code that distinguishes null from undefined may behave differently


# ============================================
# PART 10: WASM-READY PACKAGES
# ============================================
# Packages must not import dart:html or dart:js
# Use only dart:js_interop
# pub.dev filter: wasm-ready

The ten parts trace the two compilation paths and the build tooling that combines them. Parts 1 and 3 show the compiler invocations. Parts 4 and 5 show the WasmGC mapping and the dual-build loader. Part 9 shows the semantic difference that matters for interop code.


Quick Reference

Compiler Comparison

Aspectdart2jsdart2wasm
OutputSingle .js file.wasm + .mjs loader
TargetJavaScript engineWasm runtime (WasmGC)
GCBrowser’s JS GCWasmGC
Browser requirementAny JavaScript browserWasmGC-capable browser
Entrypoint<script src="main.dart.js">Loader required
Standalone modeN/A--standalone flag

Shared vs. Divergent

ComponentSharedDivergent
Frontend (parser, analyzer, Kernel IR)YesNo
Type systemYesRuntime representation differs
BackendNoSSA → JS vs. WasmGC lowering
Null semanticsNoJS null/undefined vs. single Wasm null

Build Tooling Options

OptionEffect
compiler: dart2jsJavaScript release build
compiler: dartdevcDevelopment build with incremental rebuilds
compilers: [dart2js, dart2wasm]Dual build with feature detection
loader optionCustom entrypoint name

Best Practices

✅ Do This:

# Use dart2js for maximum browser compatibility
dart compile js -O2 bin/main.dart                                       # ✅
# Use dart2wasm for performance-critical apps targeting modern browsers
dart compile wasm bin/main.dart                                         # ✅
# Use dual build for production with fallback
# build_web_compilers: compilers: [dart2js, dart2wasm]                 # ✅
# Check wasm-ready status on pub.dev for dependencies
# Filter: wasm-ready                                                   # ✅

❌ Don’t Do This:

# Assume dart2wasm output works as a direct <script> tag
# It requires a loader and a .mjs helper                              # ❌
# Use dart:html or dart:js in a package intended for Wasm
# These libraries are not Wasm-compatible                               # ❌
# Rely on null vs. undefined distinction in shared interop code
# dart2wasm internalizes both as Dart null                              # ❌

Common Pitfalls

PitfallWhy It HappensFix
Wasm module fails to loadBrowser lacks WasmGC supportUse dual build with JS fallback
dart:html import breaks Wasm buildLibrary not Wasm-compatibleUse dart:js_interop only
Null/undefined confusion in interopdart2wasm collapses both to nullTest interop behavior on both targets
No entrypoint for Wasm build.wasm needs a loaderUse build_web_compilers or generate loader
Larger output than expecteddart2js tree shaking incompleteCheck entry point; remove dead code

Real-World Examples

1. dart2js Compile

dart compile js -O2 bin/main.dart

2. dart2wasm Compile

dart compile wasm bin/main.dart

3. Standalone Wasm

dart compile wasm --standalone bin/main.dart

4. Deferred Loading

dart compile wasm --enable-deferred-loading bin/main.dart

5. Dual Build Entrypoint

Loader detects WasmGC → loads .wasm or .dart2js.js

6. WasmGC Browser Check

Chrome 137+, Safari 26.2+, Firefox 134+

7. Wasm-Ready Package

No dart:html, no dart:js; uses dart:js_interop

8. Null Interop Difference

dart2js: null and undefined distinct
dart2wasm: both become Dart null

9. SIMD in dart2wasm

Int32x4, Float32x4 backed by v128 instructions

10. dart2js SSA

Kernel IR → SSA → optimized JavaScript

Visual

The Two Compilation Pipelines

┌──────────────────────────────────────────────────────────────┐
│  DART SOURCE                                                 │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ SHARED FRONTEND                                         ││
│  │ Parser → Analyzer → Kernel IR                           ││
│  └──────────────────────────┬──────────────────────────────┘│
│                             │                                │
│           ┌─────────────────┴─────────────────┐             │
│           ▼                                 ▼               │
│  ┌─────────────────┐               ┌─────────────────┐     │
│  │ DART2JS BACKEND │               │ DART2WASM BACKEND│    │
│  │                 │               │                 │     │
│  │ SSA IR          │               │ WasmGC lowering │     │
│  │ Tree shaking    │               │ Object mapping  │     │
│  │ Type inference  │               │ SIMD support    │     │
│  └────────┬────────┘               └────────┬────────┘     │
│           ▼                                 ▼               │
│  ┌─────────────────┐               ┌─────────────────┐     │
│  │ main.dart.js    │               │ main.wasm       │     │
│  │ (single file)   │               │ main.mjs        │     │
│  └─────────────────┘               └─────────────────┘     │
│                                                              │
│  Shared frontend. Different backends. Different outputs.     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

WasmGC Object Mapping

┌──────────────────────────────────────────────────────────────┐
│  DART OBJECT                    WASMGC STRUCT               │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ Class instance  │───────────►│ Struct with     │         │
│  │                 │            │ typed fields    │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ List<int>       │───────────►│ Array of i32    │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ GC              │───────────►│ WasmGC collector│         │
│  │ (browser)       │            │ (Wasm runtime)  │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  No bundled Dart GC. The Wasm runtime manages Dart objects.  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Dual Build Loader

┌──────────────────────────────────────────────────────────────┐
│  main.dart.js (entrypoint loader)                            │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────┐                                        │
│  │ WasmGC support? │                                        │
│  └────────┬────────┘                                        │
│           │                                                  │
│     ┌─────┴─────┐                                           │
│     ▼           ▼                                           │
│  ┌──────┐   ┌──────┐                                       │
│  │ YES  │   │ NO   │                                       │
│  │      │   │      │                                       │
│  │ Load │   │ Load │                                       │
│  │ .wasm│   │ .js  │                                       │
│  │ via  │   │      │                                       │
│  │ .mjs │   │      │                                       │
│  └──────┘   └──────┘                                       │
│                                                              │
│  The loader makes the choice transparent to the application. │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Null Semantics Difference

┌──────────────────────────────────────────────────────────────┐
│  DART2JS                        DART2WASM                    │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ Dart null →     │            │ Dart null →     │         │
│  │ JS null or      │            │ Wasm null       │         │
│  │ JS undefined    │            │ (single value)  │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ JS null →       │            │ JS null AND     │         │
│  │ Dart null       │            │ JS undefined →  │         │
│  │ JS undefined →  │            │ Dart null       │         │
│  │ Dart null       │            │ (both collapse) │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  Interop code that distinguishes null from undefined         │
│  may behave differently on the two targets.                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Shared frontendParser, analyzer, Kernel IR
dart2js backendSSA IR, tree shaking, JavaScript emission
dart2wasm backendWasmGC lowering, binary module emission
dart2js outputSingle .js file
dart2wasm output.wasm + .mjs loader
WasmGC requirementChrome 137+, Safari 26.2+, Firefox 134+
Dual buildFeature detection loader chooses Wasm or JS
Standalone Wasm--standalone, no JS interop
Null differencedart2wasm collapses null and undefined
Wasm-ready packagesNo dart:html, no dart:js

Key takeaways:

  • Both compilers share the same frontend but diverge at the back end. The parser, analyzer, and Kernel IR are common. dart2js lowers to SSA and emits JavaScript. dart2wasm lowers Dart objects to WasmGC structures and emits a binary module .
  • dart2wasm relies on WasmGC to avoid bundling a collector. Before WasmGC, compiling Dart to Wasm would have required including a garbage collector in the module, which would have increased code size and hurt startup. WasmGC allows the runtime to manage Dart objects directly .
  • dart2wasm output requires a loader. The compiler produces a .wasm module and a .mjs helper, but not a <script>-ready entrypoint. The build tooling generates a loader that performs feature detection and loads the appropriate output .
  • Null and undefined collapse in dart2wasm. The JavaScript backends preserve the distinction between JS null and undefined. dart2wasm internalizes both as Dart null, which can affect interop code that depends on the difference .
  • The dual build makes the choice transparent. When both compilers are enabled, the loader checks for WasmGC support and loads the Wasm module or the JavaScript fallback accordingly. The application code does not need to know which target ran .

Remember: Dart’s web compilation strategy is a compatibility story with two paths. dart2js is the universal path: it runs on every JavaScript engine and produces a single file that any browser can load. dart2wasm is the performance path: it targets the subset of browsers with WasmGC support and produces a binary module that the Wasm runtime executes. The compilers share the same frontend, so the source code is the same. The differences are in the runtime, the output format, and the interop semantics. The build tooling exists to let a project ship both: a Wasm module for browsers that support it, and a JavaScript fallback for those that do not.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!