ZetCode

Dart WebAssembly

last modified October 22, 2026

WebAssembly (Wasm) is a compact binary instruction format that runs in modern browsers and in standalone runtimes. Dart can compile web applications to WebAssembly with the dart compile wasm command, which uses the dart2wasm compiler. The result is a .wasm module and a small JavaScript runtime that loads it.

The Wasm backend is an alternative to compiling to JavaScript. It is a good fit for CPU-heavy web applications: the browser executes the generated instructions directly, while Dart's type system and garbage collector are supported by the WasmGC proposal.

How Dart compiles to WebAssembly

Dart targets three standardized WebAssembly proposals:

The compiler emits a .wasm module plus a companion .mjs file. The JavaScript file contains the code that compiles and instantiates the module, and the entry point is invoked with invokeMain().

Browser support and requirements

Because Dart relies on WasmGC and the newer exception-handling instructions, the generated module needs a recent browser:

BrowserMinimum version
Chrome / Edge137
Safari (macOS and iOS)26.2
Firefox134 (147 for Flutter web)

Only Dart's next-generation JavaScript interop is supported in Wasm: dart:js_interop and package:web. Legacy libraries such as dart:html and package:js cannot be used. Packages that avoid those libraries are marked as wasm-ready on pub.dev.

The webdev tool does not support Wasm compilation in either webdev serve or webdev build. Compile with dart compile wasm and serve the resulting static files yourself.

Creating the project

We start with the standard web template, which already uses package:web and has an #output element:

$ dart create -t web wasm_demo
$ cd wasm_demo

Replace the generated web/main.dart with an example that computes a list and writes it into the page:

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

void main() {
  final output = web.document.querySelector('#output') as web.HTMLDivElement;
  final squares = [for (var i = 1; i <= 5; i++) i * i];
  output.textContent = 'Squares: $squares';
}

The template's web/index.html already loads main.dart.js and provides the output element. The relevant parts look like this:

<div id="output"></div>
<script defer src="main.dart.js"></script>

Compiling to Wasm

Compile the entry point into a site directory. The -O2 option enables optimizations that reduce code size and improve performance:

$ mkdir -p site
$ dart compile wasm -O2 web/main.dart -o site/main.wasm
Generated wasm module 'site/main.wasm', and JS init file 'site/main.mjs'.

The compiler writes several files:

FilePurpose
main.wasmThe compiled WebAssembly module.
main.mjsJavaScript runtime that compiles and instantiates the module.
main.support.jsFeature-detection support code for the generated runtime.
main.wasm.mapSource map for debugging.

Loading the module in the browser

Copy the static files into the output directory:

$ cp web/index.html web/styles.css site/

The template's index.html expects a main.dart.js file, so we create one that imports the generated runtime, fetches the module, and calls the Dart entry point:

site/main.dart.js
(async function () {
  const { compileStreaming } = await import('./main.mjs');
  const app = await compileStreaming(fetch('main.wasm'));
  const instantiatedApp = await app.instantiate({});
  instantiatedApp.invokeMain();
})();

compileStreaming compiles the module while it is being downloaded, instantiate creates an instance, and invokeMain runs the Dart main function. The second argument to instantiate is an optional import object; the basic example needs no extra imports.

Serving the app

Wasm modules must be served over HTTP; browsers do not load them from file:// URLs. The dhttpd package serves the site directory with the correct MIME types:

$ dart run dhttpd@ --path site
Serving site at http://localhost:8080

Opening the page shows the compiled Dart code running in the browser:

Squares: [1, 4, 9, 16, 25]

Calling JavaScript from Dart

Wasm uses dart:js_interop for JavaScript calls. The following example writes a message to the browser console:

web/main.dart
import 'dart:js_interop';

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

void main() {
  log('Hello from Dart WebAssembly!'.toJS);
}

The @JS annotation maps log to console.log, external declares that the implementation lives in JavaScript, and .toJS converts the Dart string to a JSString. Compile and serve the app as before; the browser console displays:

Hello from Dart WebAssembly!

Deferred loading

Deferred loading is experimental in Wasm. With the --enable-deferred-loading flag, the compiler splits the program into multiple modules based on its deferred imports. The loader must provide a callback that fetches the additional modules.

First, move part of the code into a deferred library:

web/lazy.dart
String message() => 'Deferred module loaded';
web/main.dart
import 'package:web/web.dart' as web;
import 'lazy.dart' deferred as lazy;

void main() async {
  final output = web.document.querySelector('#output') as web.HTMLDivElement;
  output.textContent = 'Loading...';

  await lazy.loadLibrary();
  output.textContent = lazy.message();
}

Compile with the experimental flag:

$ dart compile wasm -O2 --enable-deferred-loading web/main.dart -o site/main.wasm

The output now includes an extra module such as main_module1.wasm. Update the loader to fetch deferred modules when the runtime asks for them:

site/main.dart.js
(async function () {
  const { compileStreaming } = await import('./main.mjs');
  const app = await compileStreaming(fetch('main.wasm'));

  const instantiatedApp = await app.instantiate({}, {
    loadDeferredModules: (modules, handleModuleByteSource) => {
      return Promise.all(
        modules.map((m) =>
          fetch(m).then((r) => handleModuleByteSource(m, r))),
      );
    },
  });

  instantiatedApp.invokeMain();
})();

The page first shows Loading... and then switches to the text from the deferred library:

Deferred module loaded

Optimizing the output

The optimization level has a large effect on the size of the Wasm module. For the small squares example, compiled with Dart 3.13.5, the sizes were:

Optionmain.wasm
-O063.8 KB
-O210.2 KB
-O410.1 KB

Production builds usually use -O2 or higher. Additional options include --minify for shorter runtime names, --no-source-maps to omit the .wasm.map file, and --no-strip-wasm to keep static symbol names for debugging.

Standalone modules

For non-browser runtimes, --standalone produces a module without JavaScript interop:

$ dart compile wasm --standalone -O2 main.dart -o main.wasm

Standalone modules require a host that provides Dart's runtime imports. For Wasmtime 39 or later, enable the required proposals with -W gc,exceptions,function-references=y. Because dart:js_interop is not available in this mode, the entry point must be pure Dart and must not import browser libraries such as package:web.

Limitations

Source

Dart: WebAssembly (Wasm) compilation
Dart: JavaScript interop
Dart: dart compile

In this article, we compiled a Dart web app to WebAssembly, loaded the module in the browser, called JavaScript from Dart, and enabled experimental deferred loading. Wasm is a second compilation target for Dart web apps; the source code stays the same, but the build and loading steps differ from dart2js.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.