Flutter Web has had a reputation problem.
Teams tried it early — 2021, 2022 — ran into real issues and wrote it off. Blurry text on some browsers. Sluggish on lower-end machines. A 2MB download before the app even started. The canvas-based rendering felt wrong for the web in ways that were hard to articulate but easy to feel. The consensus in a lot of developer communities settled somewhere between "not yet" and "not for this."
That reputation is now outdated. Not because the limitations disappeared — some of them are structural and won't — but because two specific problems that made Flutter Web feel like a compromise have been addressed. WebAssembly changes the performance story in a meaningful way, and the Impeller-based rendering pipeline changes the consistency story. What remains genuinely limited is also genuinely specific, which means the right answer is no longer "avoid Flutter Web" but "use it for the right thing."
This article explains what changed, what didn't, and how to think about Flutter Web as part of a larger Flutter strategy.
What Was Actually Wrong
Two separate problems caused early Flutter Web to feel unreliable.
The JavaScript performance ceiling. Flutter's Dart code, when compiled for web, was compiled to JavaScript via dart2js. JavaScript is fast, but it's JIT-compiled at runtime — the browser's JS engine optimises code as it runs, which means performance is inconsistent. Complex widget trees, heavy computations, and rapid state updates could produce GC pauses and frame drops that weren't present in the native mobile version of the same app. The app worked, but it didn't feel the same.
Shader compilation jank on the web. The same problem that plagued Flutter on iOS before Impeller was present on web too. CanvasKit — the WebAssembly version of Skia that Flutter Web uses for rendering — compiled shaders at runtime. The first pass through a complex animation or visual effect caused a visible stutter.
WebAssembly, in its new form with WasmGC support, addresses the first problem directly. The second is addressed by the same Impeller work that fixed iOS.
What WebAssembly Actually Changes
WebAssembly (WASM) is a binary instruction format that browsers can execute at near-native speed. It's not JavaScript and it doesn't go through a JavaScript engine. The browser has a separate WASM runtime that executes WASM instructions directly, with more predictable performance characteristics than JIT-compiled JavaScript.
The limitation that kept high-level languages like Dart from compiling to WASM efficiently was garbage collection. WASM originally had no GC — it managed memory manually, like C. Dart's garbage collector couldn't run inside WASM without being compiled into the WASM binary itself, which was wasteful and slow.
WasmGC — WebAssembly Garbage Collection — solved this. Browsers now have a GC built into the WASM runtime. Dart's GC can hook into the browser's GC rather than shipping its own. High-level languages compile to clean, efficient WASM.
For Flutter, this means Dart code compiles to WASM instead of JavaScript. The Flutter framework itself compiles to WASM. The rendering pipeline runs on a WASM runtime rather than a JS engine. The result: more consistent frame times, significantly better performance for compute-heavy operations, and the elimination of the GC pauses that characterised Flutter Web on JavaScript.
To build for WASM today:
# Requires Flutter 3.22+ and a browser with WasmGC support
flutter build web --wasm
# Serve locally to test
cd build/web
python3 -m http.server 8080Browser support for WasmGC: Chrome 119+, Firefox 120+, Edge 119+, Safari 17+. For production, you need a fallback strategy for users on older browsers — Flutter's build tooling handles this automatically when you build with --wasm, serving the JS version to browsers that don't support WasmGC and the WASM version to those that do.
The Rendering Pipeline: Three Modes
Understanding which renderer Flutter Web uses — and when — matters for performance decisions.
HTML renderer — Flutter renders using HTML elements, CSS transforms, Canvas 2D, and SVG. Smallest bundle size, fastest initial load, works on any browser. But it can't reproduce Flutter's full visual fidelity. Complex effects, custom painting, and some animations are limited or absent. Best for: simple UIs, prioritising load speed, SEO-adjacent uses where you want some HTML structure.
CanvasKit renderer — Flutter renders to a WebGL canvas using Skia compiled to WASM. Pixel-perfect rendering, full Flutter visual fidelity. Downloads about 1.5–2MB of WASM on first load. This has been the default for desktop browsers since Flutter 3.
WASM with Impeller — The new path. Dart compiled to WASM, rendering via the Impeller pipeline rather than Skia. Eliminates shader compilation jank, consistent frame times, best raw performance. Requires WasmGC-capable browser.
// lib/main.dart
// No code changes needed to switch renderers — it's a build flag
// The app detects and serves the appropriate build automatically
void main() {
runApp(const MyApp());
}The renderer is selected at build time and detected at runtime. Your Dart code doesn't change between rendering modes.
When Flutter Web Makes Sense
Flutter Web with WASM is genuinely good for a specific category of applications:
Dashboards and internal tools. Admin panels, analytics dashboards, business intelligence interfaces. These are data-heavy, interaction-heavy, and their users are on known modern browsers. They don't need to rank on Google. They benefit enormously from pixel-perfect rendering and the kind of complex, interactive visualisations Flutter handles well.
Companion web experiences. A company with a Flutter mobile app can extend to web with significant code reuse. The domain layer, use cases, repositories — everything from the Clean Architecture article — transfers completely. The same business logic that runs on the mobile app runs on the web app. Separate widget implementations where needed, shared everything else.
High-performance web applications. Anything that would previously have required a heavy JavaScript framework for performance reasons — real-time data visualisation, complex interactive UIs, collaborative tools. WASM's performance envelope is closer to native than to typical JavaScript apps.
Cross-platform with web as a target. If you're already building for iOS and Android in Flutter, adding a web build is substantially cheaper than building a separate React or Vue frontend. The question "should we have a web version?" becomes "how much of our mobile code can we reuse?" rather than "do we hire a web team?"
When Flutter Web Is Still the Wrong Choice
Being clear about this is more useful than overselling.
SEO-critical content. Flutter Web renders to a canvas element. Search engine crawlers can't read canvas content the way they read HTML. Google can execute JavaScript, so some indexable content is possible, but it's limited and unreliable. If pages need to rank organically — a blog, a marketing site, a documentation portal — Flutter Web is the wrong foundation.
Accessibility requirements. Flutter Web's accessibility has improved substantially. Screen readers work. But the experience isn't at the level of native HTML. Applications where accessibility is a hard requirement — government services, healthcare tools subject to accessibility regulations — need to evaluate this carefully with real assistive technology testing.
Content-heavy, text-heavy experiences. Text selection, copy-paste behaviour, and text rendering on web have always been areas where Flutter's canvas-based approach feels subtly different from native HTML. For reading-heavy applications, users will notice.
Users on older browsers. The WASM path requires Chrome 119+, Firefox 120+, Edge 119+, Safari 17+. Consumer-facing applications with a broad or older user base need to plan for the JavaScript fallback — which is workable but slower, returning to the pre-WASM performance characteristics.
Building for Web and Mobile Together
This is the strongest enterprise argument for Flutter Web. Here's what code reuse actually looks like in a Clean Architecture project:
lib/
├── features/
│ └── orders/
│ ├── domain/ # ✅ 100% shared — pure Dart, no platform imports
│ │ ├── entities/
│ │ ├── use_cases/
│ │ └── repositories/
│ ├── data/ # ✅ Mostly shared
│ │ ├── models/ # Serialisation — identical
│ │ ├── sources/ # HTTP calls — identical (dio works on web)
│ │ └── repositories/# Implementations — identical
│ └── presentation/ # ⚠️ Partially shared
│ ├── bloc/ # ✅ BLoC logic — identical
│ ├── pages/ # ⚠️ Often needs adaptation
│ └── widgets/ # ✅ Most simple widgets — identicalThe domain layer and use cases transfer completely — they're pure Dart with no platform dependencies. BLoC logic transfers completely. HTTP calls through dio work identically on web and mobile.
Where adaptation is needed:
// lib/features/orders/presentation/pages/orders_page.dart
import 'package:flutter/foundation.dart';
class OrdersPage extends StatelessWidget {
const OrdersPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: kIsWeb
? const OrdersWebLayout() // Wider layout, sidebar navigation
: const OrdersMobileLayout(), // Standard mobile layout
);
}
}Or more granularly, using LayoutBuilder to respond to actual screen width rather than platform:
class OrdersResponsiveLayout extends StatelessWidget {
const OrdersResponsiveLayout({super.key});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1024) {
return const OrdersDesktopLayout();
} else if (constraints.maxWidth >= 600) {
return const OrdersTabletLayout();
}
return const OrdersMobileLayout();
},
);
}
}LayoutBuilder is preferable to kIsWeb checks for most layout decisions — an iPad running the mobile app at full width benefits from the tablet layout. kIsWeb is for genuinely platform-specific behaviour, not screen size decisions.
Web-Specific Adaptations
Desktop web users expect interactions that don't exist on mobile. Hover states, cursor changes, right-click menus, keyboard navigation. Flutter provides the APIs — they just need to be used.
// Hover state
MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
color: _isHovered
? Theme.of(context).colorScheme.surfaceVariant
: Colors.transparent,
child: OrderCard(order: order),
),
)// Text selection — enable where users expect to copy content
SelectionArea(
child: Column(
children: [
Text(order.reference),
Text(order.total.formatted),
],
),
)// Scrollbar — visible on web, hidden on mobile
Scrollbar(
thumbVisibility: kIsWeb, // Always visible on web, auto-hide on mobile
child: ListView.builder(
itemCount: orders.length,
itemBuilder: (context, index) => OrderCard(order: orders[index]),
),
)URL Strategy
Flutter Web by default uses hash-based routing — URLs look like yoursite.com/#/orders/123. This works but looks wrong on the web and causes issues with server-side routing. Switch to path-based URLs:
// lib/main.dart
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy(); // Before runApp
runApp(const MyApp());
}This produces clean URLs like yoursite.com/orders/123. Your server or CDN needs to redirect all paths to index.html for Flutter to handle the routing — a standard single-page app configuration.
# nginx config for Flutter Web
location / {
try_files $uri $uri/ /index.html;
}Deployment
Flutter Web builds to a static folder in build/web/. Any static hosting works — Firebase Hosting, Netlify, Vercel, S3 + CloudFront, GitHub Pages.
# Production build with WASM
flutter build web --wasm --release
# The build/web folder contains everything:
# - index.html
# - main.dart.js (JavaScript fallback)
# - main.dart.wasm (WASM build)
# - flutter_bootstrap.js (detects capability, serves appropriate version)
# - assets/
# - canvaskit/ (Skia WASM for rendering)For the WASM build, the flutter_bootstrap.js file automatically detects whether the user's browser supports WasmGC and serves the WASM version if it does, the JavaScript version if it doesn't. No configuration needed — it handles the split automatically.
One performance consideration: the initial bundle is large. CanvasKit alone is 1.5–2MB. For applications where initial load time matters, a loading screen while the WASM downloads is worth implementing:
<!-- web/index.html — customise the loading state -->
<body>
<div id="loading">
<div class="spinner"></div>
<p>Loading...</p>
</div>
<script src="flutter_bootstrap.js" async></script>
</body>The Honest Assessment
Flutter Web with WASM in 2026 is a real option for the right applications. The performance story is genuinely different from 2022. The code sharing story for teams already on Flutter is compelling.
It's not a replacement for HTML-based web development. The SEO limitation is structural — Flutter renders to canvas, not to the DOM. The accessibility gap is narrowing but real. For consumer-facing, content-driven, SEO-critical applications, you want HTML.
For internal tools, dashboards, companion experiences, and applications where pixel-perfect rendering and performance matter more than crawlability — Flutter Web with WASM deserves a serious evaluation, not the dismissal it got when the technology was younger than the reputation it earned.