All Articles
CategoryFlutter
Reading Time
15 min read
Published
2026-10-28
Word Count
3,748words

Grab a coffee — this one is a deep dive!

Flutter Web Wasm in 2026: When Should You Turn It On?

Summary

Should you use Wasm for Flutter web? A decision guide covering the --wasm flag, the dart:html to package:web migration, deferred loading, and browser support.

  • As of Flutter 3.47, Wasm is opt-in via the --wasm flag; the 3.47 announcement aims to make it the default, but it isn't yet.
  • dart:html is unsupported in Wasm compiles — migrating means moving to package:web and using dart fix --dry-run for the automatic conversion.
  • On browsers without WasmGC support (including browsers on iOS) the app falls back to JavaScript output automatically — it does not crash.
  • Deferred loading is an experimental main-channel flag only; there's no official speed/size benchmark, so base your decision on your own Chrome trace measurement.
Flutter Web Wasm in 2026: When Should You Turn It On?

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 --wasm flag to CI, run flutter 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

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:

bash
1flutter build web --release --wasm

Why "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.

bash
1flutter create . --platforms web

Warning: 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:

bash
1flutter run -d chrome --wasm

This 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%:

bash
1flutter build web --wasm --source-maps
2flutter build web --release --wasm --no-strip-wasm

This 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:

dart
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.

dart
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, like dart:html and dart: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

dart
1// Before
2import 'dart:html' as html;
3 
4void logTitle() {
5 html.window.console.log(html.document.title);
6}
dart
1// After
2import '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:

bash
1dart pub add web

Per 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:

bash
1dart fix --dry-run

The 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:

dart
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.

dart
1import 'dart:js_interop';
2 
3import 'package:web/web.dart';
4 
5bool isTextInput(Element element) {
6 // dart:html equivalent: element is InputElement
7 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.

dart
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:

dart
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:

bash
1flutter build web --release --wasm --enable-wasm-deferred-loading

Still 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:

text
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:html dependency is already absent or small and the migration cost to package:web is 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-wasm in 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:

  1. Produce two separate release builds: one without --wasm, one with --wasm.
  2. Serve both from the same static server, with the same CDN cache state.
  3. Record a performance trace in Chrome DevTools with 4x CPU throttling and Slow 4G.
  4. Compare LCP, TTI, and total JS/Wasm download size.
  5. 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

Tags

#flutter#wasm#webassembly#flutter-web#package-web#dart#performance
Muhittin Camdali

Muhittin Çamdalı

Senior Mobile Engineer

Senior Mobile Engineer with 12+ years of experience. I build mobile products with iOS, Flutter and React Native, working full-stack and AI-native.

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

Share