| |

Dart 61 🎯 Compiling Dart to WebAssembly (WASM) and Modern Browser Deployment

⏱️ Estimated read: 15 min | |

Dart has always compiled to JavaScript for the web. That path works, but it carries the limitations of JavaScript: the garbage collector, the dynamic dispatch, and the performance ceiling of the JIT. The WebAssembly target is the alternative. It compiles the Dart code to a compact binary format that the browser executes at near-native speed, with the garbage collector implemented in the Dart runtime and the DOM access through the dart:js_interop bridge. The result is the web application that loads faster, runs faster, and behaves more predictably than the JavaScript target, at the cost of the different build toolchain and the browser support baseline.

Key point: Dart’s WebAssembly target is not the standalone WASM of the C or the Rust. It is the WASM with the Dart runtime, the garbage collector, and the DOM bridge, all compiled into the one binary. The target requires the browser with the WasmGC proposal, which is the Chrome 119+, the Firefox 120+, and the Safari 18.2+.


Why the WASM target exists

The JavaScript ceiling problem. The Dart-to-JavaScript compiler produces the code that runs on the JavaScript engine. The engine’s JIT optimizes the hot paths, and the performance is the good. The ceiling is the JavaScript semantics: the dynamic types, the property lookups, and the garbage collector. The WASM target removes the ceiling by compiling to the typed binary.

The WasmGC problem. The early WebAssembly had no garbage collector. The languages with the managed memory could not compile to the WASM without shipping their own GC and the heap. The WasmGC proposal added the struct and the array types with the GC support, and the Dart, the Kotlin, and the Java could compile to the WASM natively. The Chrome shipped the WasmGC in the 119, and the Firefox in the 120, and the Safari in the 18.2.

The Dart runtime problem. The Dart’s WASM output includes the Dart runtime: the garbage collector, the type system, the async support, and the core libraries. The runtime is the compiled to the WASM and the shipped alongside the application code. The bundle is the larger than the JavaScript, and the runtime is the shared across the application.

The DOM problem. The WebAssembly has no direct DOM access. The Dart uses the dart:js_interop to call the JavaScript and the DOM APIs. The bridge is the typed, and the calls are the fast. The package:web library provides the typed bindings.

The toolchain problem. The WASM target requires the dart compile wasm command, the wasm-opt tool, and the server with the correct MIME type. The toolchain is the different from the JavaScript, and the deployment is the additional configuration.

The browser support problem. The WASM target requires the WasmGC, and the browsers that lack it cannot run the application. The JavaScript target remains the fallback, and the conditional import is the mechanism.

The Flutter problem. The Flutter web supports the WASM target. The flutter build web --wasm produces the WASM bundle, and the Skia renderer is the compiled to the WASM. The performance is the better, and the bundle is the larger.


a. The compilation

The dart compile wasm command produces the WASM module and the JavaScript loader. The output is the set of the files the browser loads.

The basic compilation

dart compile wasm main.dart -o main.wasm

The command produces the main.wasm and the main.mjs (the JavaScript loader). The main.mjs is the entry point the HTML references.

The output files

The fileThe purpose
main.wasmThe WASM module with the Dart runtime
main.mjsThe JavaScript loader and the glue
main.wasm.mapThe source map (the optional)
main.mjs.mapThe JS source map (the optional)

The compile flags

The flagThe purpose
-oThe output path
-O1The optimize for the size
-O2The optimize for the speed
-O3The maximum optimization
-O4The aggressive optimization
--no-source-mapsThe skip the source maps

The optimization levels

# The size
dart compile wasm main.dart -O1 -o main.wasm

# The speed
dart compile wasm main.dart -O4 -o main.wasm

The -O1 is the smaller, and the -O4 is the faster.

The conditional import

The JavaScript and the WASM targets share the source. The dart.library.js_interop condition selects the implementation.

export 'src/storage_none.dart'
    if (dart.library.io) 'src/storage_io.dart'
    if (dart.library.js_interop) 'src/storage_web.dart';

The dart.library.js_interop is true in the both the JavaScript and the WASM targets. The dart.library.html is the deprecated.

The dart:js_interop

The interop library is the same in the both targets. The JSString, the JSNumber, and the toJS/toDart conversions are the mechanisms.

import 'dart:js_interop';

@JS('console.log')
external void log(JSAny? message);

void main() {
  log('Hello from the WASM'.toJS);
}

The @JS annotation and the external declaration are the same as the JavaScript target.

The package:web

The package:web library provides the typed DOM bindings.

import 'package:web/web.dart' as web;

void main() {
  final div = web.document.createElement('div') as web.HTMLDivElement;
  div.textContent = 'Hello';
  web.document.body!.appendChild(div);
}

The bindings are the typed, and the calls are the interop.

The compilation table

The elementThe purpose
dart compile wasmThe command
main.wasmThe module
main.mjsThe loader
-O1 to -O4The optimization
dart:js_interopThe interop
package:webThe DOM

b. The deployment

The WASM module is the static file. The server must serve it with the correct MIME type, and the HTML must load the loader.

The server MIME type

The .wasm file must be served with the application/wasm MIME type. The wrong type prevents the streaming compilation.

# The nginx
types {
    application/wasm wasm;
}

The types block maps the extension to the MIME type.

The HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>The Dart WASM</title>
</head>
<body>
  <script type="module">
    import { compileStreaming } from './main.mjs';
    // The loader is the entry.
  </script>
</body>
</html>

The main.mjs is the loader, and the browser fetches the main.wasm.

The streaming compilation

The WebAssembly.compileStreaming compiles the module while the bytes are the streaming. The server must serve the application/wasm for the streaming.

const response = await fetch('main.wasm');
const module = await WebAssembly.compileStreaming(response);

The compileStreaming is the faster than the WebAssembly.instantiate with the buffered bytes.

The caching

The .wasm file is the immutable, and the content hash is the cache key.

location ~* \.wasm$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

The header tells the browser and the CDN to cache the file forever. The new build produces the new hash.

The compression

The .wasm file compresses well. The gzip and the brotli are the standard.

gzip on;
gzip_types application/wasm;
brotli on;
brotli_types application/wasm;

The compression reduces the transfer size by the 50% or more.

The conditional loading

The browser that lacks the WasmGC must fall back to the JavaScript. The feature detection and the conditional load.

<script type="module">
  if (WebAssembly.validate(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]))) {
    // The WASM is the supported.
    import('./main.mjs');
  } else {
    // The JS fallback.
    import('./main.js');
  }
</script>

The WebAssembly.validate checks the bytecode. The WasmGC detection is the more specific.

The deployment table

The elementThe purpose
application/wasmThe MIME type
main.mjsThe loader
compileStreamingThe streaming
The immutableThe cache
The gzip/brotliThe compression
The fallbackThe JS

c. The performance and the comparison

The WASM target is the faster, and the bundle is the larger. The trade-offs are the specific.

The startup

The WASM startup involves the fetch, the compile, and the instantiate. The compile is the fast, and the instantiate is the the initialization of the Dart runtime.

The fetch:      the network
The compile:    the streaming
The instantiate: the runtime init
The run:        the main

The runtime

The runtime is the Dart’s garbage collector, the type system, and the async. The runtime is the compiled to the WASM, and the startup is the the runtime init.

The performance

The WASM is the faster than the JavaScript for the CPU-bound code. The DOM access is the interop, and the cost is the bridge.

The workloadThe JavaScriptThe WASM
The CPU-boundThe goodThe better
The DOM-heavyThe goodThe comparable
The startupThe fastThe slower
The bundleThe smallerThe larger

The bundle size

The WASM bundle is the larger because the runtime is the included. The JavaScript bundle is the smaller because the runtime is the browser’s.

The JS bundle:  the ~1-2 MB
The WASM bundle: the ~2-4 MB

The gzip and the brotli reduce the transfer.

The browser support

The browserThe WasmGC
The Chrome119+
The Firefox120+
The Safari18.2+
The Edge119+

The older browsers need the JavaScript fallback.

The Flutter integration

flutter build web --wasm

The --wasm flag produces the WASM bundle. The Skia is the compiled to the WASM, and the performance is the better.

The comparison table

The aspectThe JavaScriptThe WASM
The performanceThe goodThe better
The bundleThe smallerThe larger
The startupThe fastThe slower
The supportThe universalThe modern
The DOMThe nativeThe interop
The GCThe engine’sThe Dart’s

Complete Example Session

// ============================================
// PART 1: THE DART SOURCE
// ============================================

import 'dart:js_interop';
import 'package:web/web.dart' as web;

void main() {
  final div = web.document.createElement('div') as web.HTMLDivElement;
  div.textContent = 'Hello from the WASM';
  div.className = 'flex items-center p-4';
  web.document.body!.appendChild(div);
}

// ============================================
// PART 2: THE COMPILATION
// ============================================

// dart compile wasm main.dart -o main.wasm

// The output:
//   main.wasm
//   main.mjs

// ============================================
// PART 3: THE OPTIMIZATION
// ============================================

// The size
// dart compile wasm main.dart -O1 -o main.wasm

// The speed
// dart compile wasm main.dart -O4 -o main.wasm

// ============================================
// PART 4: THE HTML
// ============================================

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>The Dart WASM</title>
</head>
<body>
  <script type="module" src="./main.mjs"></script>
</body>
</html>

// ============================================
// PART 5: THE SERVER MIME TYPE
// ============================================

// The nginx
types {
    application/wasm wasm;
}

// ============================================
// PART 6: THE CACHING
// ============================================

location ~* \.wasm$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

// ============================================
// PART 7: THE COMPRESSION
// ============================================

gzip on;
gzip_types application/wasm;

brotli on;
brotli_types application/wasm;

// ============================================
// PART 8: THE CONDITIONAL LOAD
// ============================================

<script type="module">
  if (WebAssembly.validate(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]))) {
    import('./main.mjs');
  } else {
    import('./main.js');
  }
</script>

// ============================================
// PART 9: THE FLUTTER WASM
// ============================================

// flutter build web --wasm

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

// 1. Use the dart:js_interop for the DOM.
// 2. Use the package:web for the typed bindings.
// 3. Use the dart compile wasm to build.
// 4. Use the -O1 for the size, the -O4 for the speed.
// 5. Serve the application/wasm MIME type.
// 6. Use the compileStreaming for the fast startup.
// 7. Cache the .wasm with the immutable header.
// 8. Compress with the gzip or the brotli.
// 9. Detect the WasmGC and the fall back.
// 10. The Flutter uses the --wasm flag.

The ten parts showed the Dart source, the compilation, the optimization, the HTML, the server MIME type, the caching, the compression, the conditional load, the Flutter WASM, and the complete pattern.


Quick Reference

The Compilation

The commandThe purpose
dart compile wasmThe build
-oThe output
-O1 to -O4The optimization
--no-source-mapsThe skip

The Output Files

The fileThe purpose
main.wasmThe module
main.mjsThe loader
main.wasm.mapThe source map

The Server Config

The elementThe purpose
application/wasmThe MIME type
The immutableThe cache
The gzipThe compression
The brotliThe compression

The Browser Support

The browserThe WasmGC
The Chrome119+
The Firefox120+
The Safari18.2+

The Interop

The elementThe purpose
dart:js_interopThe core
package:webThe DOM
toJS/toDartThe conversion
@JSThe annotation

Best Practices

✅ Do This:

// Use the dart:js_interop.
import 'dart:js_interop';  // ✅
// Use the package:web.
import 'package:web/web.dart' as web;  // ✅
# Use the correct optimization.
dart compile wasm main.dart -O4 -o main.wasm  // ✅
# Serve the application/wasm.
types { application/wasm wasm; }  // ✅
# Cache the .wasm.
add_header Cache-Control "public, max-age=31536000, immutable";  // ✅
# Compress the .wasm.
brotli_types application/wasm;  // ✅
<!-- Detect the WasmGC and the fall back. -->
<script type="module">
  if (WebAssembly.validate(...)) import('./main.mjs');
  else import('./main.js');
</script>  // ✅

❌ Don’t Do This:

# Don't serve the .wasm as the application/octet-stream.
# The streaming compilation is disabled.  // ❌
# Don't skip the optimization.
dart compile wasm main.dart  // ⚠️ The larger
// Don't use the dart:html in the new code.
import 'dart:html';  // ❌ The deprecated
<!-- Don't forget the fallback. -->
<script src="./main.mjs"></script>  // ❌ The no fallback
# Don't cache the .mjs forever.
# The loader is the updated.  // ⚠️ The hash the .mjs too
// Don't assume the WASM is the always faster.
// The DOM-heavy is the comparable.  // ❌
# Don't forget the MIME type.
# The server serves the octet-stream.  // ❌

Common Pitfalls

The pitfallThe reasonThe fix
The streaming disabledThe wrong MIMEServe application/wasm
The startup slowThe large runtimeUse the -O4
The browser errorThe no WasmGCAdd the fallback
The stale cacheThe no hashUse the content hash
The large transferThe no compressionUse the gzip/brotli
The DOM slowThe interop overheadMinimize the calls
The bundle largeThe runtimeAccept or the JS

Real-World Examples

1. The Basic Compilation

dart compile wasm main.dart -o main.wasm

2. The Optimization

dart compile wasm main.dart -O4 -o main.wasm

3. The HTML

<script type="module" src="./main.mjs"></script>

4. The MIME Type

types { application/wasm wasm; }

5. The Caching

add_header Cache-Control "public, max-age=31536000, immutable";

6. The Compression

brotli_types application/wasm;

7. The Conditional Load

<script type="module">
  if (WebAssembly.validate(...)) import('./main.mjs');
  else import('./main.js');
</script>

8. The Flutter WASM

flutter build web --wasm

9. The package:web

import 'package:web/web.dart' as web;

10. The Complete Pattern

dart compile wasm main.dart -O4 -o main.wasm

Visual

The Dart WASM Pipeline

┌────────────────────────────────────────────────────────────┐
│  THE DART WASM PIPELINE                                    │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE DART SOURCE                                   │     │
│  │    The main.dart                                   │     │
│  │    The dart:js_interop, the package:web            │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE dart compile wasm                             │     │
│  │    The -O4, the -o main.wasm                       │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE OUTPUT                                        │     │
│  │    The main.wasm (the runtime + the app)           │     │
│  │    The main.mjs (the loader)                       │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE SERVER                                        │     │
│  │    The application/wasm                            │     │
│  │    The gzip/brotli, the immutable                  │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE BROWSER                                       │     │
│  │    The compileStreaming                            │     │
│  │    The instantiate, the run                        │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Bundle Comparison

┌────────────────────────────────────────────────────────────┐
│  THE BUNDLE COMPARISON                                     │
│                                                            │
│  THE JAVASCRIPT                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The app: the ~1-2 MB                              │     │
│  │  The runtime: the browser's                        │     │
│  │  The startup: the fast                             │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE WASM                                                  │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The app + the runtime: the ~2-4 MB                │     │
│  │  The runtime: the Dart's                           │     │
│  │  The startup: the slower                           │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The WASM is the larger and the faster at the steady.      │
└────────────────────────────────────────────────────────────┘

The Browser Support

┌────────────────────────────────────────────────────────────┐
│  THE BROWSER SUPPORT                                       │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The Chrome 119+                                   │     │
│  │  The Firefox 120+                                  │     │
│  │  The Safari 18.2+                                  │     │
│  │  The Edge 119+                                     │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE FALLBACK                                              │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The older browser → the JS bundle                 │     │
│  │  The WebAssembly.validate → the detection          │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Deployment

┌────────────────────────────────────────────────────────────┐
│  THE DEPLOYMENT                                            │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The main.wasm                                     │     │
│  │    → the application/wasm MIME                     │     │
│  │    → the immutable cache                           │     │
│  │    → the gzip/brotli                               │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The main.mjs                                      │     │
│  │    → the loader                                    │     │
│  │    → the shorter cache                             │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The index.html                                    │     │
│  │    → the conditional load                          │     │
│  │    → the no-cache                                  │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

Summary

The itemThe value
dart compile wasmThe compilation
The main.wasmThe module
The main.mjsThe loader
The application/wasmThe MIME type
The compileStreamingThe fast startup
The dart:js_interopThe interop
The package:webThe DOM
The -O1 to -O4The optimization
The WasmGCThe browser requirement
The fallbackThe older browser

Key takeaways:

  • The Dart’s WASM target is the WASM with the Dart runtime. The runtime is the garbage collector, the type system, and the core libraries, compiled to the WASM and shipped with the application. The bundle is the larger, and the performance is the better.
  • The WasmGC is the requirement. The Chrome 119+, the Firefox 120+, and the Safari 18.2+ support the garbage collector in the WASM. The older browsers need the JavaScript fallback.
  • The dart compile wasm produces the two files. The main.wasm is the module, and the main.mjs is the JavaScript loader. The HTML references the loader, and the loader fetches the module.
  • The application/wasm MIME type is the requirement for the streaming. The wrong type disables the compileStreaming, and the startup is the slower. The nginx types block is the configuration.
  • The dart:js_interop and the package:web are the DOM mechanisms. The interop library is the same in the both the JavaScript and the WASM targets, and the bindings are the typed.
  • The optimization levels trade the size for the speed. The -O1 is the smaller, and the -O4 is the faster. The choice depends on the workload.
  • The compression and the caching are the deployment. The gzip and the brotli reduce the transfer, and the immutable cache reduces the repeat. The content hash is the cache key.
  • The Flutter supports the WASM. The flutter build web --wasm produces the WASM bundle, and the Skia is the compiled to the WASM. The performance is the better for the Flutter web.

Remember: The Dart’s WASM target is the alternative to the JavaScript. It compiles the Dart to the WASM with the runtime, and the browser executes it at the near-native speed. The dart compile wasm produces the module and the loader, the application/wasm MIME type enables the streaming, and the dart:js_interop bridges the DOM. The result is the web application that is the faster and the more predictable, at the cost of the larger bundle and the modern browser baseline.


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!