Flutter Web's Wasm-First Era: What Changes and How to Migrate
WebAssembly is slated to become Flutter web's default compilation target. What dart2wasm changes, how to read the performance claims, and a concrete migration checklist from dart2js and legacy JS interop.
Published on • October 11, 2026
AI Assistant

For years, “Flutter web is heavy” was the reflexive critique, and the reflex was not unfair: a dart2js bundle, a large runtime, and a JIT that warms up on the main thread do not add up to a snappy first paint. That era is closing. Flutter’s 2026 roadmap states the intent without hedging: “On the web, we intend for WebAssembly (Wasm) to become the default to deliver native-quality experiences and performance.” The toolchain already ships flutter run -d chrome --wasm and flutter build web --wasm, and every regular JS build now performs a silent Wasm “dry run” that warns you about incompatibilities before the default flips.
The interesting question is no longer whether Wasm is coming, but what actually changes for your app: which code must be rewritten (anything touching dart:html), which browsers can run the result (not all of them, iOS included), and how to weigh the performance claims floating around — including widely shared numbers like “2x to 5x faster” — that deserve attribution, not repetition.
In this tutorial, you will learn how to:
- Explain what dart2wasm changes relative to dart2js, and where CanvasKit fits
- Attribute and evaluate reported Wasm performance claims instead of repeating them
- Check browser compatibility, including the WebKit and Firefox limitations
- Build and verify a Wasm web app with
flutter build web --wasmand thedart.tool.dart2wasmenvironment check - Serve a multithreaded Wasm build correctly with COOP/COEP headers
- Migrate off
dart:htmlandpackage:jstopackage:webanddart:js_interop - Read Wasm dry-run and compiler errors without drowning in the stack trace
- Work through a migration checklist, and know when the JS build still matters
Key technologies: dart2wasm, dart2js, flutter build web --wasm, package:web, dart:js_interop, CanvasKit, WasmGC, COOP/COEP headers.
Prerequisites
- Flutter 3.24 or later (the minimum the Wasm docs name for compiling to Wasm), ideally current stable
- A web-enabled Flutter project with an up-to-date
web/index.htmlinitialization (regenerate withflutter create . --platforms webif unsure) - Chrome or another WasmGC-capable browser for development
- An HTTP server you control for production-header testing
What “Wasm-first” actually means
Dart has two web compilers. They are not interchangeable:
| dart2js | dart2wasm | |
|---|---|---|
| Output | JavaScript | WebAssembly (WasmGC) |
| Type info at runtime | Erased, JS-typed | Preserved in Wasm types |
| JS interop | dart:html, package:js era APIs | package:web, dart:js_interop only |
| Browser floor | Broad | WasmGC required (Chrome/Edge 119+, Firefox 120+) |
| Status in 2026 | Fallback and default today | Roadmap default; opt-in via --wasm |
Two behaviors of the current toolchain are easy to miss. First, even with --wasm, the build still compiles the JavaScript output; if WasmGC support is not detected at runtime, the JS bundle runs instead. Your deployable artifact is therefore dual by design, which is why compatibility questions soften into “slower fallback” rather than “blank page” — on platforms where JS runs at all. Second, Wasm is not automatic: you opt in per build, and you verify with an explicit check, not vibes:
const isRunningWithWasm = bool.fromEnvironment('dart.tool.dart2wasm');
The docs also note the classic identical(double.nan, double.nan) trick as a secondary signal of native number representation, but the compile-time environment flag is the preferred check.
Performance claims, with attribution
Numbers travel faster than footnotes, so here is what is actually on record:
- The Flutter team’s own 2026 roadmap frames Wasm as the path to “native-quality experiences and performance,” without publishing a multiplier.
- A widely shared Medium analysis from April 2026 (“Flutter 2026: The Death of Native Development?” by Jeffery Alexandro Henry) reports a 2x–3x performance boost in complex animations and data processing for Wasm-based Flutter web, and cites benchmark claims that Flutter Wasm apps execute logic at roughly 85–90% of native C++ speed. Treat both as that author’s reported figures for specific workloads, not guarantees.
- Community discussion and secondary posts often round this up to a “2x–5x” range. That range is a summary of heterogeneous benchmarks, not a Flutter-published SLA.
The honest engineering position: Wasm removes JIT warm-up variance and shrinks parse/compile overhead for the Dart code itself, but your app’s performance is still dominated by rendering, layout, and network. Measure your own app in DevTools against your own baseline. If your workload is animation- and compute-heavy, the reported ranges are plausible; if your app is a form over a REST API, they are irrelevant.
Renderers and the threading story
Flutter web paints through CanvasKit — the Skia-based renderer compiled to JavaScript — and the Wasm line of work pairs naturally with canvas-based rendering: precompiled modules, predictable startup, no shader-compilation surprises mid-animation. The direction of travel mirrors Impeller on mobile: make the renderer’s behavior uniform across frames.
Threading is where Wasm earns its keep for jank-sensitive UIs. The Wasm docs state plainly that Flutter web apps compiled with WebAssembly “can use multiple threads to render your application faster, with less jank” — but those threads only start if the server sends the right isolation headers:
| Header | Required value |
|---|---|
Cross-Origin-Embedder-Policy | credentialless or require-corp |
Cross-Origin-Opener-Policy | same-origin |
Skipping this is the most common “why is my Wasm build still single-threaded in production” moment. Test it with DevTools against your deployed host, not against flutter run’s dev server.
Building and verifying
The development loop:
$ flutter run -d chrome --wasm
The production build:
$ flutter build web --wasm
Output lands in build/web, same as a JS build. Useful production flags, straight from the docs:
# Symbolication for error trackers (recommended in production):
$ flutter build web --wasm --source-maps
# Keep function names in stack traces for staging (about +47% wasm size):
$ flutter build web --wasm --no-strip-wasm
# Experimental deferred loading (main branch; documented as landing in the 3.50 stable):
$ flutter build web --wasm --enable-wasm-deferred-loading
Release builds strip debug symbols and omit source maps by default. --source-maps emits main.dart.wasm.map for services that can symbolicate; --no-strip-wasm trades a documented ~47% binary-size increase for readable function names in the browser console. Neither belongs in a size-sensitive production artifact without a reason.
The development loop on web
A default flip only happens if the daily loop survives it. Two parts of that loop matter:
Hot reload. Stateful hot reload — the workflow that preserves app state across edits — has historically been a mobile-and-desktop story; on the web it arrived later and has been rolling out across recent Flutter releases. The 2026 roadmap treats keeping “core workflows like stateful hot reload” working seamlessly (including for AI coding agents) as part of the developer-experience investment, which is a signal: the team considers the web loop load-bearing for Wasm-default to succeed. If hot reload behavior differs between your JS and Wasm dev builds, verify with the Flutter tool’s own channel notes rather than assuming parity.
Compile feedback. dart2wasm compiles your app to a binary module; there is no JS bundle to eyeball in devtools. Debug with source maps (--source-maps), browser Wasm-aware tooling, and print/logging as usual — and remember that release Wasm builds strip names by default, so keep the debug flags straight between environments.
Migrating dependencies: the real blocker
Wasm compilation rejects dart:html and package:js. The replacements are package:web (successor to dart:html and friends) and dart:js_interop (successor to package:js/dart:js). Your app code may be clean while a transitive dependency is not — which is why the dry run matters.
Even plain flutter build web (without --wasm) now performs a Wasm dry run and prints non-fatal warnings:
Wasm dry run failed:
Found incompatibilities with WebAssembly.
package:my_app/main.dart 1:1 - dart:html unsupported (0)
When a full --wasm build fails, ignore the long Target dart2wasm failed... stack trace and scroll to the structured context tree:
Context: The unavailable library 'dart:html' is imported through these packages:
main.dart => package:my_app => dart:html
During a transition, conditional imports let one codebase serve both compilers:
import 'fallback.dart'
if (dart.library.js) 'legacy_web_interop.dart'
if (dart.library.js_interop) 'wasm_web_interop.dart';
Also audit for JS interop runtime differences the docs call out: is/as type-cast behavior and Zone propagation in callbacks are not identical across the JS and Wasm interop paths. Code that “worked on web” by relying on loose JS semantics can fail its first Wasm cast.
Migration checklist
- Upgrade to Flutter 3.24+ and regenerate
web/index.htmlif it predates the current initialization template. - Run a plain
flutter build weband read the Wasm dry-run warnings; triage by package, app code first. - Replace
dart:htmlusage withpackage:web, andpackage:js/dart:jswithdart:js_interop. - Check every plugin and package for Wasm compatibility; the “wasm-ready” topic on pub.dev is a useful filter.
- Build with
flutter build web --wasmlocally and fix the structured dependency-tree errors until it succeeds. - Verify at runtime that
bool.fromEnvironment('dart.tool.dart2wasm')is true in your target browser matrix. - Configure COOP/COEP on your host and confirm multithreaded rendering actually engages.
- Add
--source-mapsif you ship error monitoring; keep--no-strip-wasmout of production. - Retest Safari, Firefox, and iOS expectations against your analytics — see the compatibility table below.
Browser compatibility: the asterisk
WasmGC support is the hard requirement. Per the Flutter Wasm docs:
- Chromium and V8 have supported WasmGC since version 119.
- Firefox 120 announced stable WasmGC support, but a known bug currently blocks Flutter’s Wasm renderer (tracked in Mozilla’s bug tracker).
- Safari supports WasmGC but has a similar blocking bug for Flutter’s renderer.
- No browser on iOS can run the Wasm build — all iOS browsers must use WebKit, which cannot run Flutter’s Wasm renderer today.
Because the JS output ships alongside, users on unsupported browsers fall back rather than fail — at dart2js performance, with dart2js bundle weight. Budget for both artifacts in your CDN configuration.
When JS still matters
The JS build is not a legacy appendage in 2026; it is the compatibility layer. Keep caring about it while: iOS Safari is a material share of your traffic; a dependency has no Wasm path yet; or you depend on embedding Flutter in an existing JS app where the JS engine path is simpler to operate.
SEO and accessibility deserve their own note. A Flutter web app renders into a canvas: search engines and assistive technologies do not see a DOM tree of headings and buttons. Flutter exposes a semantics tree for screen readers, and it works — but indexable content strategy (server-rendered landing pages, prerendered routes, meaningful <title>/meta in index.html) is unaffected by which compiler you choose. Wasm-first does not make a single-page canvas app crawlable; plan content separately.
Common pitfalls
- Assuming
--wasmmeans “Wasm only.” The JS fallback is always built; verify withdart.tool.dart2wasm, not the build command’s exit code. - Shipping without COOP/COEP. Single-threaded rendering in production with a “fast” Wasm build is a configuration bug, not a compiler regression.
- Auditing only your own code. The failure mode is almost always a transitive plugin importing
dart:html. - Over-indexing on headline multipliers. The 2x–3x and 85–90%-of-native figures are reported benchmarks from community analysis, not per-app promises.
- Forgetting the dry run in CI. Treat Wasm dry-run warnings as build output your pipeline surfaces, so the default flip surprises nobody.
Further reading
- Flutter & Dart’s 2026 roadmap — the Wasm-default intent
- Support for WebAssembly (Wasm) — build flags, headers, browser matrix
- Web support for Flutter — how web fits the platform story
- Build and release a web app — deployment and source-map guidance
- package:web migration guide — leaving
dart:htmlbehind - Flutter 2026: The Death of Native Development? — the source of the 2x–3x and 85–90% figures cited above