Converting a Flutter web project's compile target from JavaScript to WebAssembly looks as simple as one flag (--wasm), but it's really a threshold with three separate decisions: moving your package ecosystem to package:web, accepting gaps in browser support, and deciding whether to ship a compile path that isn't yet stable. This article answers "should I use Wasm for Flutter web" as of September 2026, based on official Flutter/Dart docs, without fabricated benchmarks.
💡 Pro Tip: Before adding the--wasmflag to CI, runflutter build web(without the flag) first — Flutter automatically performs a "Wasm dry run" and shows incompatible packages in advance with a non-fatal warning.
Table of Contents
- Current State of Wasm in Flutter Web
- Why "not default, opt-in" matters
- Where to Start Experimenting: A Ready Demo, index.html, and flutter run
- Preparing index.html
- Development and testing: flutter run --wasm
- Build and Output Differences With --wasm
- The cost of preserving function names
- Verifying you're actually running on Wasm
- Migrating from dart:html to package:web
- dart:html isn't the only blocking library
- Step-by-step migration
- Automatic conversion and the List incompatibility
- Element or HTMLElement?
- Runtime behaviors that change under Wasm
- Conditional imports for the transition period
- Splitting Modules With Experimental Deferred Loading
- Still maturing
- Automatic compatibility check
- Browser Support and Fallback Behavior
- The fallback is automatic, silent, and transparent
- Header requirements for multi-threaded rendering
- Bundle Size and the First-Load Reality
- How to produce your own data
- Which Projects Should Enable It, Which Shouldn't
- Measurement: Before/After With a Real Chrome Trace
- FAQ
- Can Flutter web Wasm be used in production?
- How do I migrate from dart:html to package:web?
- How much faster is Flutter web with Wasm?
- How do I enable Wasm deferred loading?
- Does the app crash on older browsers when I enable Wasm?
- Conclusion
- Sources
Current State of Wasm in Flutter Web
In the Flutter 3.47 announcement dated August 12, 2026, the team set the goal clearly under "Setting course for Wasm by default": making WebAssembly the default compile target. This doesn't mean it's the default now, though — in the team's own words, they're "actively working toward enabling WebAssembly (Wasm) by default," meaning you're still in the opt-in stage.
The minimum version for Wasm compilation is Flutter 3.24+; since 3.47 is well above that, there's no practical version barrier. Trigger the opt-in compile with:
1flutter build web --release --wasmWhy "not default, opt-in" matters
The gap between "we're working to make it the default" and actually being the default directly affects your production decision. As long as Wasm stays opt-in: your existing CI pipeline keeps working with JS compilation, adding --wasm is a conscious choice, and rolling back means removing one flag.
Where to Start Experimenting: A Ready Demo, index.html, and flutter run
If you want to see a working Flutter app compiled with Wasm before touching your own project, the docs point to a ready example: the Wonderous demo app is live with Wasm output. Opening it in your target browsers lets you observe browser-side support in minutes, without touching your own code.
Preparing index.html
The first file to check before switching to Wasm is your app's web/index.html. The docs require it explicitly: it must use the current Flutter web app initialization format that applies for Flutter 3.22 and later. A years-old, hand-edited index.html becomes a silent failure source here — the app keeps working on JS while it can get stuck on the Wasm path.
If you don't mind the default version, the docs offer a shortcut: delete and regenerate the web/ directory.
1flutter create . --platforms webWarning: if you've added a custom script tag, meta tag, or splash screen to index.html, this command removes those too. I prefer committing this step separately and reviewing the diff line by line — that keeps it clear what to restore.
Development and testing: flutter run --wasm
--wasm isn't build-only. The docs also describe using it with flutter run for development and testing:
1flutter run -d chrome --wasmThis has real practical value: weeks before adding --wasm to CI, you run your daily dev loop on Wasm and catch incompatibilities one by one, while they're fresh. A team's "let's try Wasm" decision shouldn't start with CI — it should start with a developer running flutter run -d chrome --wasm for a week.
No surprise on output: flutter build web --wasm writes to build/web under the package root, same as flutter build web. No path changes needed in deployment scripts, CI artifacts, or CDN uploads.
Build and Output Differences With --wasm
With --wasm, the output directory holds a set of .wasm files plus supporting JS instead of a single JS bundle. This dual output is what makes the automatic JavaScript fallback possible, kicking in when the browser doesn't support WasmGC:
"Even with the --wasm flag, Flutter will still compile the application to JavaScript. If WasmGC support is not detected at runtime, the JavaScript output is used so the application will continue to work in all major browsers."So a --wasm compile produces two outputs, not one: the Wasm path and the JS fallback path. Whichever the user's browser supports is what runs.
The cost of preserving function names
Wasm release builds strip debug symbols by default and skip source maps. The docs recommend two separate paths: --source-maps for symbolication in error-tracking services (production), and --no-strip-wasm for readable console function names (staging/QA). The latter has a concrete cost — it grows the Wasm binary by roughly 47%:
1flutter build web --wasm --source-maps2flutter build web --release --wasm --no-strip-wasmThis alone shows why "keep --no-strip-wasm on everywhere" is wrong: turn it on in staging, switch to --source-maps in production.
Verifying you're actually running on Wasm
To confirm the compile went to Wasm, check the dart2wasm environment variable — the method the Flutter team recommends:
1const bool runningOnWasm =2 bool.fromEnvironment('dart.tool.dart2wasm');The docs mark this path "preferred." They also describe an alternative: testing whether the native (Wasm) number representation is in use, based on differences in how numbers are represented.
1final isRunningWithWasm = identical(double.nan, double.nan);The difference isn't just style. The first is a compile-time constant usable in const contexts, so tree shaking can prune branches based on it; the second is a runtime final, so it can't go where a const is required. If you'll use the flag to split code paths and not just to log, stay on the bool.fromEnvironment side.
Migrating from dart:html to package:web
The biggest practical obstacle to Wasm compilation isn't performance — it's your codebase's dependency on dart:html. Dart's official docs state this explicitly:
"Dart core web libraries, likedart:htmlanddart:svg, are deprecated and not supported when compiling to Wasm."
The solution is package:web — it's compatible with Wasm because it's built on dart:js_interop.
dart:html isn't the only blocking library
The most common way to leave a migration half-finished is assuming the scope is only dart:html. Flutter's docs draw the boundary clearly: "This prevents Dart code that uses dart:html or package:js from compiling to Wasm." Code using package:js also can't compile to Wasm, and its counterpart isn't package:web but a separate fix.
Old API | Wasm-compatible counterpart |
|---|---|
dart:html, dart:svg, and other core web libraries | package:web |
package:js, dart:js | dart:js_interop |
In practice this means two scans: searching import lines for dart:html isn't enough — also search for @JS() annotations and the package:js dependency. Even if your own code has neither, a package in your dependency tree might — the automatic dry run below exists exactly for this.
Step-by-step migration
1// Before2import 'dart:html' as html;3 4void logTitle() {5 html.window.console.log(html.document.title);6}1// After2import 'dart:js_interop';3 4import 'package:web/web.dart' as web;5 6void logTitle() {7 web.console.log(web.document.title.toJS);8}Note: console is a top-level getter in package:web, not under window. Since $Console.log expects JSAny?, converting the String with .toJS — meaning the dart:js_interop import — is required.
The dart.dev migration guide's command is enough to add the package:
1dart pub add webPer pub.dev, the current version is 1.1.1 (published February 26, 2025) — reaching 1.0 more than two years before this article, so it's a settled package.
Automatic conversion and the List incompatibility
You don't need to do most name changes by hand:
1dart fix --dry-runThe one snag: values returned by package:web methods like Element.querySelectorAll and Element.children don't implement List. Passing this to an API that requires List means wrapping it with JSImmutableListWrapper.
Element or HTMLElement?
dart:html renamed some types to fit Dart style; package:web uses the original IDL names instead. The example you'll hit most: per the dart.dev guide, Element is now HTMLElement, but Element still exists as the common base type for HTMLElement, SVGElement, and others. querySelector returns an Element because it can also return subtypes other than HTMLElement — accessing HTMLElement-specific members needs a downcast:
1import 'package:web/web.dart';2 3void highlightSelected(Element element) {4 // Valid in both libraries: className is a base Element member.5 element.querySelector('#selectme')!.className = 'test';6 7 // style is specific to HTMLElement, so a downcast is required.8 (element.querySelector('#selectme') as HTMLElement).style.color = 'red';9}This rule affects every DOM-touching helper function, not just one line. Before migrating, search your codebase for querySelector calls and list which use HTMLElement-specific members — much faster than chasing compile errors one by one afterward.
Runtime behaviors that change under Wasm
Migration isn't done once import lines change. Flutter's docs warn separately: compiling to Wasm needs care around "runtime differences in JS interop types (such as is/as type casts and Zone propagation in callbacks)." Two concrete differences.
First, type tests. With dart:html types, is/as verified whether an object matched the JS type in its @Native annotation. All package:web types materialize as JSObject, so the same runtime test doesn't give the same result. The dart.dev guide's counterpart: instanceOfString, or the more typed isA helper since Dart 3.4.
1import 'dart:js_interop';2 3import 'package:web/web.dart';4 5bool isTextInput(Element element) {6 // dart:html equivalent: element is InputElement7 return element.isA<HTMLInputElement>();8}The second is sneakier, since it produces no compile error: in dart:html, callbacks were auto-bound to the zone; in package:web, they aren't. Code relying on zone-bound values needs the callback bound manually.
1import 'dart:async';2import 'dart:js_interop';3 4import 'package:web/web.dart' as web;5 6void listenZoned(web.Element element) {7 element.addEventListener(8 'click',9 Zone.current.bindUnaryCallback((web.Event event) {10 // Zone-bound values are preserved here.11 }).toJS,12 );13}These two differences explain the gap between "it compiles" and "it works correctly." Don't verify a migration by compile output alone — test type-test- and zone-dependent flows at the behavioral level.
Conditional imports for the transition period
If you need to support both the old and new interop layers at the same time:
1import 'fallback.dart'2 if (dart.library.js) 'legacy_web_interop.dart'3 if (dart.library.js_interop) 'wasm_web_interop.dart';Splitting Modules With Experimental Deferred Loading
The 3.47 announcement brought experimental deferred loading support specific to Wasm: the ability to split large web apps into smaller, lazily loaded modules. This feature is currently only on the main channel, behind a flag:
1flutter build web --release --wasm --enable-wasm-deferred-loadingStill maturing
Concrete evidence: issue #193069 in flutter/flutter — on September 20, 2026, this command produced Could not find an option named "--enable-wasm-deferred-loading". on stable channel Flutter 3.47.5; closed the same day. The practical conclusion: this flag doesn't work outside main, and you shouldn't rely on it in production before it lands on stable.
Automatic compatibility check
Even if you run flutter build web without adding --wasm, Flutter performs a Wasm dry run in the background; if there's an incompatible package, it prints a non-fatal warning. The warning looks exactly like this:
1Wasm dry run failed:2Found incompatibilities with WebAssembly.3 4package:my_app/main.dart 1:1 - dart:html unsupported (0)Note: this dry-run warning has no structured dependency tree — just file, line, and the unsupported library's name. The structured Context tree is separate output: when flutter build web --wasm fails to compile, it appears above the long exception stack trace, showing which package still uses dart:html as a chain like main.dart => package:my_app => dart:html. The docs' advice is exactly this: ignore the long stack trace, scroll to the top.
Browser Support and Fallback Behavior
The fact that a Wasm build compiles doesn't mean the Wasm path will be used in every browser. Flutter's Wasm output requires WasmGC (Wasm Garbage Collection), and support for this isn't equal across browsers.
Browser | WasmGC Support | Note |
|---|---|---|
Chromium / V8-based (Chrome, Edge) | Available since version 119 | Wasm path works |
Firefox 120+ | Technically available | A known bug blocks compatibility with Flutter's Wasm renderer |
Safari | WasmGC is now supported | A similar compatibility bug blocks the Flutter renderer |
Chrome for iOS | Not available | WasmGC isn't supported yet since it uses the WebKit engine |
The fallback is automatic, silent, and transparent
This table looks scary, but in practice users notice nothing: a --wasm-compiled app automatically falls back to JS output on a browser without WasmGC. Firefox 120+ and Safari do have WasmGC, but a separate bug blocking Flutter's Wasm renderer has been reported — so if you target these two browsers, verify the behavior with your own measurement.
This has a measurement cost: you can't guess how many users are actually on the Wasm path, you have to measure it. The bool.fromEnvironment('dart.tool.dart2wasm') constant only tells you whether the compile went to Wasm, not which path the user's browser fell back to. To see the real distribution, collect analytics events broken down by browser and version and compute the WasmGC-supporting share yourself — otherwise "we turned on Wasm" may mean nothing for most of your traffic.
Header requirements for multi-threaded rendering
If you want to enable multi-threaded rendering with Wasm, your server needs to send these headers:
Header | Value |
|---|---|
Cross-Origin-Embedder-Policy | credentialless or require-corp |
Cross-Origin-Opener-Policy | same-origin |
Bundle Size and the First-Load Reality
Let's be honest: Flutter and Dart's official sources publish no benchmark for how much Wasm changes first-load time or bundle size versus JS. The blog post uses only a qualitative phrase like "native-like performance," with no concrete millisecond or KB figure.
The only official number we have points toward a size increase: --no-strip-wasm grows binary size by roughly 47% to preserve function names. Concrete proof you shouldn't blindly trust a general claim like "Wasm produces smaller output."
How to produce your own data
Instead of a fabricated benchmark, measure your own project: compare the same page in --wasm and non---wasm builds via a Chrome DevTools trace under 4x CPU throttling and Slow 4G. That's the only reliable method — don't trust general percentages from third-party posts.
Which Projects Should Enable It, Which Shouldn't
If we need to boil down all the technical detail so far into a single decision table: turning on Wasm is an investment with an uncertain payoff but a concrete cost. The cost side is clear — the package:web migration, the index.html update, observability setup. The payoff side is both officially unquantified and dependent on your user base's browser distribution. The list below turns this balance into concrete cases:
- Enable: Computation-heavy dashboards or game-like interfaces with many widget rebuilds; if your user base is mostly desktop Chrome and Edge.
- Enable: If your
dart:htmldependency is already absent or small and the migration cost topackage:webis low. - Don't enable (for now): If your user base has a significant share of Safari or iOS Chrome traffic — iOS Chrome silently falls back to JS, and on Safari you can't count on the Wasm path working because of the renderer bug.
- Don't enable (for now): If you have a large app that needs deferred loading — this feature is experimental on the main channel, not on stable.
- Don't enable (for now): If you don't have observability infrastructure that can read production errors without
--no-strip-wasmin CI; stack traces are harder to read under Wasm than under JS.
Measurement: Before/After With a Real Chrome Trace
Saying "it got faster" without comparing two builds — --wasm on and off — in the same environment would be falling into the same fabricated-benchmark mistake this article criticized at the start. Steps:
- Produce two separate release builds: one without
--wasm, one with--wasm. - Serve both from the same static server, with the same CDN cache state.
- Record a performance trace in Chrome DevTools with 4x CPU throttling and Slow 4G.
- Compare LCP, TTI, and total JS/Wasm download size.
- Interpret the result against your own project's real user distribution (which browser, which device) — a general percentage doesn't transfer to another project.
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
Since you read this article to the end, below is a short checklist you can use when deciding whether to migrate to Wasm. You can copy the list as-is onto your own project board.
FAQ
Can Flutter web Wasm be used in production?
Partially. The --wasm flag is an officially opt-in compile path available in Flutter 3.24 and above; you can produce a production build with flutter build web --release --wasm, but the Flutter team hasn't made it the default yet — the 3.47 announcement says they're "actively working toward enabling WebAssembly (Wasm) by default." Scalability features like deferred loading are experimental on the main channel and not yet recommended for production.
How do I migrate from dart:html to package:web?
You need to remove the dart:html import, add package:web/web.dart, and add the web package to pubspec.yaml. dart fix --dry-run automatically shows most of the name changes; for return types that don't implement List, you may need to use JSImmutableListWrapper.
How much faster is Flutter web with Wasm?
No concrete percentage has been published in official sources — giving a hard number here would be fabrication. Flutter's official statement is qualitative, like "native-like performance"; the only concrete official figure goes the other way, with the --no-strip-wasm flag increasing binary size by roughly 47%. Only your own Chrome trace measurement shows the real payoff for your project.
How do I enable Wasm deferred loading?
With flutter build web --release --wasm --enable-wasm-deferred-loading, but this is an experimental flag only available on the main channel — not on stable or beta. As seen in flutter/flutter issue 193069, stable 3.47.5 doesn't recognize this flag; I'd recommend not shipping it to production before it lands on stable.
Does the app crash on older browsers when I enable Wasm?
No. When a browser without WasmGC support is detected, Flutter automatically falls back to the JavaScript output and the app keeps working — you just don't get Wasm's potential performance benefit.
Conclusion
Turning on Wasm is technically possible, but you're carrying three separate maturity levels at once: the compile path is on the stable channel but opt-in, deferred loading is experimental, and browser support is partial. Moving your codebase to package:web together with the same version wave as the Flutter 3.47: Migrating to the material_ui and cupertino_ui Packages article is a good opportunity to do both migrations at once. For the render engine's overall performance picture, see Flutter 4 Impeller: The New Render Engine and Performance Revolution; for modern syntax changes on the Dart side, see Dart 3.13 Primary Constructor and Dart 3 Innovations. If you want a broader performance framework around the 60fps target, the Flutter Performance Optimization: 60fps Guarantee Guide article, and if you want to see how architectural decisions ease a Wasm migration, the Flutter Clean Architecture article will complement this one.
Sources
- Flutter 3.47 announcement — Wasm default target, the --wasm flag, and the deferred loading announcement.
- Flutter Web Wasm documentation — browser support, fallback behavior, the --no-strip-wasm cost.
- package:web migration guide — dart:html unsupport, dart fix, JSImmutableListWrapper.
- package:web pub.dev page — package version and release date.
- Flutter release notes — minimum version and general version tracking.
- flutter/flutter issue #193069 — the --enable-wasm-deferred-loading flag not being recognized on stable 3.47.5.
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy
