Flutter Web Renderer Selection After the --web-renderer Flag
What the removal of Flutter's --web-renderer flag means for your build: CanvasKit and Skwasm today, the JavaScript vs WebAssembly choice, browser fallbacks, and server headers.
Published on • November 12, 2026
AI Assistant

For years the first performance question on any Flutter web deployment was whether to pass --web-renderer html or --web-renderer canvaskit. That question no longer exists: Flutter 3.29 removed the flag entirely, and every current Flutter web app paints through the canvas-based engines instead. What remains is a different and more consequential choice, JavaScript or WebAssembly, plus the browser and server conditions attached to it. CanvasKit Rendering Engine already covers what CanvasKit does inside the pixel pipeline, and Flutter Web SEO Optimization covers indexing, so this post is only about making the selection correctly in a post-flag world.
What the flag removal changed
The 3.29 release notes tell the story in three commits: the tool removed the deprecated --web-renderer parameter, the docs removed references to the HTML renderer, and the engine dropped the HTML renderer benchmarks. Passing the old flag to a current SDK fails the way removed flags do, with an unknown-option error rather than a warning. The practical consequences are:
- There is no DOM-based output anymore, so no path to smaller, text-shaped HTML from Flutter itself.
- Both surviving engines render into a canvas using hardware-accelerated browser graphics APIs, WebGL and WebGPU, which is why rendering fidelity is now a constant and performance work moved to compile targets and engine choice.
- Any article, script, or CI job still setting
--web-rendererneeds to delete it before it will build at all.
The HTML renderer was the DOM-based option: it painted text into real HTML elements, which produced smaller payloads and indexable markup at the cost of visual drift from the mobile build. Its removal means the fidelity you see on a phone is the fidelity you get in the browser, and every remaining trade-off lives at the compile target layer rather than in a rendering mode.
The real choice: JavaScript versus WebAssembly
The only renderer-adjacent switch left on the command line is the compilation target:
$ flutter build web # dart2js, paired with CanvasKit
$ flutter build web --wasm # dart2wasm, paired with the Wasm engine, plus a JS fallback
Even --wasm still emits JavaScript. The build produces a dual-target bundle, main.dart.wasm with its main.dart.mjs runtime module plus main.dart.js, and at startup flutter_bootstrap.js probes the browser before choosing. If the required features are missing or the browser is not in the wasmAllowList, it falls back to the dart2js and CanvasKit output automatically. You can check which target actually ran from Dart:
const isRunningWithWasm = bool.fromEnvironment('dart.tool.dart2wasm');
Build mode still drives compiler effort in both worlds:
| Build mode | With --wasm | JavaScript default |
|---|---|---|
--release (default) | -O2, stripped symbols | -O4, minified |
--profile | -O2 --no-strip-wasm | -O4, unminified symbols |
--debug | -O0, assertions on | -O1, assertions on |
Engines behind the target
CanvasKit is the Skia renderer compiled to JavaScript, and it serves every default build. Skwasm is Skia compiled to WebAssembly, running beside dart2wasm output and able to use multiple threads when the server cooperates. Neither engine is selectable through a renderer flag anymore, they arrive attached to the compile target you picked, so engine tuning belongs in the deployment layer rather than the build command.
Browsers decide the outcome
For Flutter 3.50 and later, a Wasm run requires WasmGC, JS String Builtins, and Phase 4 exception handling. The documented thresholds are:
| Engine | Minimum for Wasm | Default behavior |
|---|---|---|
| Chromium (Chrome, Edge, Opera, Brave) | 137+ | Runs Wasm by default |
| WebKit (Safari, all iOS browsers) | 26.2+ | Falls back to dart2js and CanvasKit until wasmAllowList.webkit is enabled |
| Gecko (Firefox) | 147+ | Falls back to dart2js and CanvasKit until wasmAllowList.gecko is enabled |
Older versions fall back by runtime feature detection, which means a --wasm build still works everywhere, but only Chromium users actually receive it until you opt the other engines in. Enabling them is a loader configuration change, not a build flag:
_flutter.loader.load({
config: {
wasmAllowList: { webkit: true, gecko: true },
},
});
Server requirements for the Wasm path
Multi-threaded Wasm rendering only activates with cross-origin isolation, so return Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: credentialless or require-corp on the document response. Serve .wasm files as application/wasm, pre-compress the compiled modules, and pass --no-web-resources-cdn if you would rather bundle the engine into build/web/canvaskit/ than fetch it from a CDN at runtime.
Compatibility work before enabling it
Wasm compilation rejects legacy interop, so dart:html, dart:js, and package:js must give way to package:web and dart:js_interop. A plain flutter build web already runs an automatic Wasm dry run and prints incompatibilities without failing, and Flutter 3.50 added --no-deprecated-js-interop to make those imports a compile error on every target. Conditional imports should key off dart.library.js_interop, which is the check that works under both targets.
Verifying what actually shipped
Treat the output directory as the source of truth rather than trusting the command you remember running:
- A plain release always contains
main.dart.jsand the CanvasKit engine files. - A
--wasmrelease additionally containsmain.dart.wasmandmain.dart.mjsbeside the JavaScript fallback. - In a running session, the environment check tells you whether the Wasm path executed, and the network panel shows whether the
.wasmmodule or the JavaScript fallback was fetched.
Choosing quickly
| Situation | Build |
|---|---|
| Unknown audience, long-tail browsers | flutter build web and rely on CanvasKit |
| Modern browsers, CPU-bound Dart logic | flutter build web --wasm with cross-origin headers |
| Mixed audience | --wasm, keep the fallback bundle, verify with the environment check |
| Embedded or third-party pages | Ship the JS target and let the host page stay in control |
Takeaway: The renderer flag is gone, so stop asking which renderer to pick and start asking which target to ship: default to the JavaScript and CanvasKit build for maximum reach, add --wasm with COOP and COEP headers and modern interop when your analytics show capable browsers, and keep the automatic fallback intact so the same URL serves every visitor.
Sources
- https://docs.flutter.dev/platform-integration/web.md
- https://docs.flutter.dev/platform-integration/web/building.md
- https://docs.flutter.dev/platform-integration/web/wasm.md
- https://docs.flutter.dev/platform-integration/web/faq.md
- https://docs.flutter.dev/deployment/web.md
- https://docs.flutter.dev/release/release-notes/release-notes-3.29.0.md