Flutter web: what you need to know in 2026
By Charlin Joe · 16 December 2025
Flutter web has come a long way. In 2026 the CanvasKit renderer is stable, WASM compilation is in stable channel, and most production Flutter packages work on web. But web still has distinct trade-offs that mobile developers run into. Here is what you need to know before shipping.
Renderer choice: HTML vs CanvasKit vs WASM
Flutter web has three rendering modes:
| Mode | How | When to use |
|---|---|---|
html | DOM + Canvas 2D | Deprecated in Flutter 3.22+ |
canvaskit | WebAssembly + Skia | Default for most apps |
skwasm | WASM + multi-threaded Skia | Best performance, requires COOP/COEP headers |
Build with a specific renderer:
flutter build web --web-renderer canvaskit
flutter build web --web-renderer skwasm # Fastest, needs headers
For skwasm, your server must serve these headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Loading time: the biggest pain point
CanvasKit downloads a ~1.5MB WASM binary before any Flutter content renders. Users on slow connections see a blank screen for 2–5 seconds.
Mitigations:
1. Customize the loading screen in web/index.html:
<script>
window.addEventListener('flutter-first-frame', () {
document.getElementById('loading').remove();
});
</script>
<div id="loading">
<img src="/splash.png" />
<p>Loading...</p>
</div>
2. Preload the WASM binary with a link tag:
<link rel="preload" href="canvaskit/canvaskit.wasm" as="fetch" crossorigin="anonymous">
3. Use deferred loading for large features not needed on first render:
import 'package:my_app/heavy_feature.dart' deferred as heavy;
Future<void> loadFeature() async {
await heavy.loadLibrary();
heavy.HeavyFeature(); // Now safe to use
}
URL strategy
By default Flutter web uses hash-based URLs (/#/home). For clean URLs, configure the path URL strategy:
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy();
runApp(const MyApp());
}
Your server must also redirect all paths to index.html (single-page app behaviour). For Nginx:
location / {
try_files $uri $uri/ /index.html;
}
For Firebase Hosting, in firebase.json:
{
"hosting": {
"rewrites": [{"source": "**", "destination": "/index.html"}]
}
}
SEO and meta tags
Flutter CanvasKit renders into a <canvas> — search engines see nothing. For SEO-critical content:
- Use
flutter_meta_seoto inject<meta>tags dynamically - Pre-render with a headless browser using
flutter_ssr(experimental) - Or serve a static HTML shell for crawlers and use Flutter for the interactive app
import 'package:flutter_meta_seo/flutter_meta_seo.dart';
void setMeta(String title, String description) {
MetaSEO().config(
title: title,
description: description,
ogTitle: title,
ogDescription: description,
);
}
Platform detection
import 'package:flutter/foundation.dart';
bool get isWeb => kIsWeb;
bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
// Conditional behaviour
Widget buildScaffold(Widget body) {
if (kIsWeb) {
return WebLayout(body: body);
}
return MobileLayout(body: body);
}
dart:html and dart:js interop
To interact with browser APIs, use dart:js_interop (the modern API, replacing dart:html):
import 'dart:js_interop';
@JS('localStorage')
external JSObject get _localStorage;
extension LocalStorageExtension on JSObject {
external String? getItem(String key);
external void setItem(String key, String value);
}
void saveToLocalStorage(String key, String value) {
_localStorage.setItem(key, value);
}
String? readFromLocalStorage(String key) {
return _localStorage.getItem(key);
}
Responsive web design
Desktop browsers are wide, and users resize windows. Use breakpoints:
class Breakpoints {
static const mobile = 600.0;
static const tablet = 900.0;
static const desktop = 1200.0;
}
extension BreakpointContext on BuildContext {
double get screenWidth => MediaQuery.of(this).size.width;
bool get isMobile => screenWidth < Breakpoints.mobile;
bool get isTablet => screenWidth >= Breakpoints.mobile && screenWidth < Breakpoints.desktop;
bool get isDesktop => screenWidth >= Breakpoints.desktop;
}
// Usage in widgets
if (context.isDesktop) {
return const TwoColumnLayout();
}
return const SingleColumnLayout();
Web-specific packages to avoid
Some packages don't support web:
path_provider— no filesystem on web; useshared_preferencesorlocalStorageflutter_secure_storageweb support is limited- Native camera, Bluetooth, background services — not available
sqflite— no SQLite on web; usedriftwith its web backend instead
Check pub.dev's platform support matrix before adding any package.
Performance tips
Image formats. Use WebP instead of PNG for 25–35% smaller files. Use Image.network with cacheWidth/cacheHeight to resize at decode time.
Canvas vs DOM text. CanvasKit renders text in canvas — browser find-in-page (Ctrl+F) won't find it. The HtmlElementView widget can embed real DOM elements for searchable text.
Font loading. Register only the font weights you use. Loading all 9 weights of a variable font adds ~1MB.
// In pubspec.yaml — only load weights you need
fonts:
- family: Inter
fonts:
- asset: fonts/Inter-Regular.ttf
weight: 400
- asset: fonts/Inter-SemiBold.ttf
weight: 600
Common pitfalls
Not handling browser back/forward. go_router handles this correctly. Custom navigation stacks don't — the browser back button exits the app instead of going back.
dart:io imports. Any import of dart:io crashes on web. Use conditional imports:
import 'io_stub.dart' if (dart.library.io) 'dart:io';
CORS errors. Flutter web makes requests from the browser — CORS headers are enforced. Your API must include Access-Control-Allow-Origin for the Flutter web origin. This doesn't affect mobile.
Sign in to like, dislike, or report.