← Articles

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:

ModeHowWhen to use
htmlDOM + Canvas 2DDeprecated in Flutter 3.22+
canvaskitWebAssembly + SkiaDefault for most apps
skwasmWASM + multi-threaded SkiaBest 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_seo to 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; use shared_preferences or localStorage
  • flutter_secure_storage web support is limited
  • Native camera, Bluetooth, background services — not available
  • sqflite — no SQLite on web; use drift with 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.

Flutter web: what you need to know in 2026 — ANN Tech