Flutter Web Wasm vs JavaScript Build: Which One Should You Ship?
Last Friday I was sitting in the Chrome network tab, watching our Flutter web app download a 1.5 MB canvaskit engine before a single widget painted. I had been meaning to try the Wasm build since Flutter made it a one-flag affair, and I kept finding reasons to skip it. Honestly, most of those reasons turned out to be stale. This post is what I learned shipping flutter build web --wasm to a real app, including the parts that bit me.
What the --wasm flag actually builds
First, a mental model fix. Since Flutter 3.29 removed the old HTML renderer and the --web-renderer flag, you no longer pick a renderer directly. The renderer follows the compile target: Dart compiled to JavaScript runs on canvaskit, Dart compiled to WebAssembly runs on skwasm. So the real question is "JavaScript build or Wasm build," not "canvaskit or skwasm."
The tooling enforces this. Try mixing flags and it refuses:
flutter build web --wasm --dart-define=FLUTTER_WEB_USE_SKIA=true
# Do not attempt to set a web renderer when using "--wasm"
A Wasm build emits both pipelines into build/web. The generated flutter_bootstrap.js carries a config that lists them in priority order:
"builds": [
{ "compileTarget": "dart2wasm", "renderer": "skwasm",
"mainWasmPath": "main.dart.wasm", "jsSupportRuntimePath": "main.dart.mjs" },
{ "compileTarget": "dart2js", "renderer": "canvaskit",
"mainJsPath": "main.dart.js" }
]
The loader walks the list and takes the first compatible entry. Browsers with WasmGC support get skwasm; everyone else falls back to the canvaskit build from the same output folder. One build, no routing logic on your side.
The speedup is real, but it depends on your scene
The engine download is smaller on the Wasm path. Brotli-compressed, skwasm comes in at about 1.2 MB against roughly 1.5 MB for the Chromium-tuned canvaskit variant. The frame numbers matter more. Independent benchmarks published in the last few months land in a consistent range: roughly 40% faster scrolling on average, up to 2x on frame rendering, and the biggest gains on the worst-case frames, where graphics-heavy scenes improved by up to 3x. A benchmark app in one September write-up rendered 36% more frames than canvaskit on a heavy custom-paint scene.
Our app leans on animated charts and custom painters, so it sits near the top of those ranges. A mostly static content page would see almost nothing. Be suspicious of anyone quoting a single universal number here.
The headers everyone forgets
Here is the part I got wrong on the first deploy. Multi-threaded skwasm, the fast path, needs SharedArrayBuffer. That requires your page to be cross-origin isolated, which is a server config, not a Flutter setting:
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
Once require-corp is on, every cross-origin resource the page loads must carry CORP or CORS headers, or the browser blocks it. Our analytics script died instantly. If auditing every third-party tag is not realistic, credentialless is a looser COEP value that strips credentials from cross-origin requests instead of blocking them outright. It was enough for us.
Four gotchas I hit
Dependencies first. If any package still imports dart:html or package:js, dart2wasm fails at compile time, not at runtime. Migrating to package:web and dart:js_interop took me an afternoon; most popular plugins have already done it.
Second, the dev loop. On the Wasm path there is no service protocol and hot reload is really a hot restart, so keep doing daily work with a plain flutter run -d chrome and treat Wasm as a release concern.
Third, Firefox and Safari users get canvaskit even though both engines support WasmGC today. Flutter's loader keeps them off skwasm because the frame handoff via OffscreenCanvas.transferToImageBitmap is still slow in those engines, and the relevant Firefox and WebKit bugs remain open. Do not promise Wasm to a Safari-heavy audience yet.
Fourth, a current-stable caveat: on the 3.47 line, multi-threaded skwasm can crash on text-heavy frames. The workaround is running single-threaded until the fix lands in stable; the issue tracker walks through it. Test your densest text screens before flipping the switch in production.
So should you ship it?
If your app is animation or custom-paint heavy and your dependencies compile clean, yes, ship --wasm now. You get a smaller engine download, real frame gains on capable browsers, and a free fallback for the rest. If your Flutter web app is forms and text, the win is marginal and waiting is reasonable.
One more reason to set this up soon: deferred loading for Wasm builds, currently experimental, reaches stable general availability in Flutter 3.50 due in November. Cold start on the Wasm path is about to get better, and when it does I would rather already have the pipeline and headers in place than be doing all of this under deadline pressure. I keep the nginx block and the bootstrap config stashed in Snippet Ark for exactly that reason, since every deploy environment needs the same two header lines.
If you are tuning Flutter performance more broadly, I wrote earlier about fixing ListView jank and what changed in Flutter 3.47's Material packages. The web side and the native side are converging faster than most people realize.