Skip to content
Blog

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-renderer needs 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 modeWith --wasmJavaScript 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:

EngineMinimum for WasmDefault 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.js and the CanvasKit engine files.
  • A --wasm release additionally contains main.dart.wasm and main.dart.mjs beside the JavaScript fallback.
  • In a running session, the environment check tells you whether the Wasm path executed, and the network panel shows whether the .wasm module or the JavaScript fallback was fetched.

Choosing quickly

SituationBuild
Unknown audience, long-tail browsersflutter build web and rely on CanvasKit
Modern browsers, CPU-bound Dart logicflutter build web --wasm with cross-origin headers
Mixed audience--wasm, keep the fallback bundle, verify with the environment check
Embedded or third-party pagesShip 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