skip to content

What does Flutter's flutter build web --wasm produce, when do browsers fall back to JavaScript, and why do COOP and COEP headers matter?

level: middleimportance: should knowfreq 40%

answer

  1. two builds in one output
  2. WasmGC, WebGL, allowed engine
  3. iOS browsers are all WebKit
  4. cross-origin isolation enables threads
  5. dart:html and package:js block Wasm

basics

~20 s

flutter build web --wasm emits a dart2wasm build on skwasm plus a dart2js build on CanvasKit. Browsers without WasmGC, WebGL or default Wasm enablement get the JavaScript build. Without COOP and COEP headers skwasm still runs, but single-threaded.

solid answer

~40 s

`flutter build web --wasm` writes two builds into `build/web`: the app compiled by dart2wasm, rendered by skwasm, and the app compiled by dart2js, rendered by CanvasKit. At page load the `flutter.js` loader takes the Wasm build only if the browser supports WasmGC and WebGL and its engine is on the Wasm allow list, which by default enables Chromium-based browsers only; everyone else, including every iOS browser, gets JavaScript. **COOP** (`Cross-Origin-Opener-Policy: same-origin`) and **COEP** (`Cross-Origin-Embedder-Policy: require-corp` or `credentialless`) make the page cross-origin isolated, which skwasm needs to render on web workers. Without them skwasm does not fall back; it runs single-threaded and logs a console warning. Separately, code importing `dart:html` or `package:js` cannot compile to Wasm; migrate it to `package:web` and `dart:js_interop`.

code

bash · 2 lines
bash
flutter build web --wasm
flutter build web --wasm --source-maps

go deeper

for a junior

Recall that --wasm builds both a Wasm and a JavaScript version and that browsers without WasmGC get the JavaScript one.

for a middle

Explain the loader's fallback checks, what COOP and COEP enable, and why dart:html and package:js block a Wasm compile.

for a senior

Show you can roll out Wasm safely: verify which build users run, weigh isolation headers against embedded third-party content, and plan interop migration.

for a principal

Weigh Wasm's performance gain against browser coverage, hosting constraints and dependency migration cost for the product's audience.

## What the flag builds `flutter build web` compiles Dart to JavaScript with **dart2js** and renders with **CanvasKit**. Adding `--wasm` (help text: "Compile to WebAssembly (with fallback to JavaScript)") makes the tool produce **two** compilations into the same `build/web` directory: - a **dart2wasm** build, `main.dart.wasm` with its `main.dart.mjs` support module, rendered by **skwasm**; - a **dart2js** build, `main.dart.js`, rendered by **CanvasKit**, as the fallback. The generated `flutter_bootstrap.js` carries a build configuration listing both, in that order. Because the renderer follows the compile target, the tool refuses a `--wasm` build that also forces a non-default renderer. ## When the browser gets the JavaScript build `_flutter.loader.load()` takes the first build the browser can run. The Wasm build is skipped when: | Check | Why it matters | |---|---| | **WasmGC** is missing | dart2wasm output uses WebAssembly garbage collection | | **WebGL** is missing | skwasm draws through WebGL | | The browser engine is not on the **Wasm allow list** | by default only Chromium-based engines are enabled; Firefox and Safari are off because of engine bugs that block Flutter's Wasm renderer | | The page forces another renderer in its config | a configured `renderer` that differs from the build's | All browsers on iOS must use WebKit, so a Flutter Wasm app never runs as Wasm there. The fallback is automatic and silent; with the loader's verbose build-selection option it logs why each build was skipped. ## COOP, COEP and threads skwasm can move rendering work to **web workers**, which share memory with the main thread through `SharedArrayBuffer`. Browsers expose that only to pages that are **cross-origin isolated**, and a page becomes isolated when the server sends: | Header | Value | |---|---| | `Cross-Origin-Opener-Policy` | `same-origin` | | `Cross-Origin-Embedder-Policy` | `require-corp` or `credentialless` | Without them, skwasm **still runs**, in single-threaded mode, and prints a console warning naming the two headers. It does not fall back to CanvasKit. The runtime config `forceSingleThreadedSkwasm` forces single-threaded mode deliberately, for example when the headers would break embedded third-party content. The catch with COEP: every cross-origin resource the page loads, such as images, scripts and iframes, must then permit embedding, or it is blocked. `credentialless` relaxes this by loading cross-origin resources without credentials. ## Code that cannot compile to Wasm dart2wasm supports only the static JS interop model: 1. `dart:html` and the other old web libraries are replaced by **`package:web`**. 2. `package:js`, `dart:js` and `dart:js_util` are replaced by **`dart:js_interop`**. 3. A plain `flutter build web` runs a **Wasm dry run** by default (`--wasm-dry-run`) and warns about such imports before you switch. 4. When a `--wasm` build fails, the useful part of the output is the `Context` tree showing which package imported the unsupported library. ## Rolling Wasm out in practice 1. Build without `--wasm` first and read the dry-run warnings; migrate or replace packages that import the old web libraries. 2. Build with `--wasm`, serve it with the isolation headers on staging, and confirm in Chrome's console that no single-threaded warning appears. 3. Check that embedded third-party content still loads under COEP; if it does not, choose `credentialless` or accept single-threaded rendering. 4. Keep testing Safari and Firefox, because those users stay on the JavaScript build. ## Checking what users actually run - `const bool.fromEnvironment('dart.tool.dart2wasm')` is `true` only in the Wasm build. - Wasm release builds strip symbols; `--source-maps` emits a map for error reporting and `--no-strip-wasm` keeps names at a size cost.

  • After adding COEP: require-corp, a Flutter web dashboard's images from a partner CDN stop loading. Why, and what are the options?
    Under `require-corp` every cross-origin subresource must opt in to being embedded, and the CDN does not. Either have it send the opt-in headers, switch to `Cross-Origin-Embedder-Policy: credentialless`, which loads such resources without credentials, or drop isolation and accept single-threaded skwasm, optionally setting `forceSingleThreadedSkwasm` to silence the choice.
  • A Flutter package imports dart:html. What happens when you build with --wasm?
    The dart2wasm compile fails, because `dart:html` is unavailable to Wasm. Migrate the code to `package:web` and `dart:js_interop`, or use a conditional import on `dart.library.js_interop`. A plain `flutter build web` already warns about this through its default Wasm dry run.

saying these in an interview costs you the question

  • Without COOP and COEP headers the app falls back to CanvasKit.
  • A --wasm build contains only WebAssembly, so old browsers break.
  • Chrome on iOS runs the Wasm build because it is Chrome.
  • dart:html code compiles to Wasm unchanged.
  • COEP headers have no effect on other resources the page loads.