Flutter on Web & Desktop
Irbisa · cheatsheetSeptember 13, 2026

Flutter on Web & Desktop

Web renderers and wasm, deferred loading, PWA, JS interop, desktop windowing, plugin gaps

Senior Developer16 itemscompressed for a skim
  1. 01

    An empty flutter build web already weighs a few megabytes — what is in there, and what do canvaskit and skwasm change about it?

    Easy

    Most of that weight is the Flutter engine: the browser has no Flutter in it, so every first-time visitor downloads a renderer before your first widget can exist.

    flutter build web --release
    
    # build/web
    #   index.html                  <base href> and <script src="flutter_bootstrap.js" async>
    #   flutter_bootstrap.js        generated per build: build config + the loader
    #   flutter.js                  the loader itself, exposes window._flutter
    …
  2. 02

    Your lead read that Flutter compiles to WebAssembly and asks why the production build still ships JavaScript — what do you tell them?

    Medium

    Because --wasm is opt-in and still emits JavaScript beside the wasm: Flutter builds both outputs into one deploy and the loader picks at runtime, so JS is what a large share of your users actually run.

    // One import that works under dart2js and dart2wasm.
    // dart.library.html is a trap: it is false for dart2wasm, so a condition on it
    // silently selects the stub in exactly the build you were trying to enable.
    import 'interop_stub.dart'
        if (dart.library.js_interop) 'interop_web.dart';
    …
  3. 03

    Users see a white page for two seconds and then the whole app at once — what runs between the HTML arriving and the first Flutter frame?

    Medium

    Four stages, and nothing you write in Dart can paint before all of them finish: the HTML loads, flutter_bootstrap.js chooses a build, the entrypoint and the engine download, and only then does runApp produce a frame.

    import 'package:flutter/material.dart';
    import 'package:flutter/scheduler.dart';
    import 'package:web/web.dart' as web;
    
    // ❌ every await here is white screen the user pays for before any pixel
    Future<void> main() async {
    …
  4. 04

    You put deferred as in front of the admin console and main.dart.js barely shrank — what happened?

    Medium

    dart2js moves a library into its own part file only when every path to it is deferred; one ordinary import anywhere in the program pulls the whole thing back into the main output unit.

    // ❌ deferred, and yet nothing splits: the eager route table names the type,
    // so the whole library is reachable without loadLibrary() and stays in main.
    import 'admin/admin_page.dart' deferred as admin;
    import 'admin/admin_page.dart' show AdminPage; // <- the leak
    
    final router = GoRouter(routes: [
    …
  5. 05

    The app moved from the root to example.com/console/, and now reloading a deep link 404s while nested routes lose their assets — what has to change?

    Medium

    Three separate things are broken and each has its own fix: the URL strategy in Dart, <base href> in the built HTML, and a server that must return index.html for paths with no file behind them.

    import 'package:flutter/material.dart';
    import 'package:flutter_web_plugins/url_strategy.dart';
    import 'package:go_router/go_router.dart';
    
    void main() {
      // Real paths instead of /#/settings. Must be called before runApp().
    …
  6. 06

    Marketing says none of your pages show up in Google and every shared link previews the same title — what can you actually do about that?

    Medium

    Nothing from inside the Flutter app: your content is painted into a canvas and your <head> is one static file, so a crawler and a link scraper both receive the same empty shell no matter which route was requested.

    # What a scraper sees: the same shell for every route
    curl -s https://app.example.com/pricing | grep -iE '<title>|og:title|og:description'
    # <title>app</title>          <- identical for /pricing, /blog/x, /login
    
    # Fix at the edge, not in Dart: generate one shell per route at deploy time
    build_shell() {                       # $1 = route, $2 = title, $3 = description
    …
  7. 07

    You deploy a fix in the morning and half your users are still running yesterday's build in the afternoon — where is the old bundle coming from?

    Medium

    From caching you now own yourself: recent Flutter versions stopped generating flutter_service_worker.js and the loader no longer registers any worker, so what is left is the plain HTTP cache — plus, on long-time users' machines, the old worker that is still installed.

    # What your users' browsers were told last deploy
    curl -sI https://app.example.com/ | grep -i cache-control
    curl -sI https://app.example.com/main.dart.js | grep -i cache-control
    
    # nginx: revalidate the entry points, cache the versioned engine forever
    #   location = /index.html            { add_header Cache-Control "no-cache"; }
    …
  8. 08

    A dart:io File call guarded with if (!kIsWeb) compiles for Android and breaks the web build — why does the guard not help?

    Medium

    Because kIsWeb is a runtime constant inside a file that has already imported dart:io, and dart:io does not exist on web — the build fails at the import, long before your branch could be eliminated.

    // ❌ the import fails on web; the runtime guard never gets a chance
    import 'dart:io';
    import 'package:flutter/foundation.dart';
    
    Future<String> load(String path) async {
      if (!kIsWeb) return File(path).readAsString();
    …
  9. 09

    A helper imports dart:html and the wasm build refuses to compile — what replaced it, and how do you call a JS SDK from Dart today?

    Medium

    dart:html, dart:js and dart:js_util are deprecated and unsupported under dart2wasm: the DOM now comes from package:web, and everything else goes through dart:js_interop.

    // ❌ pre-3.7 style — neither of these compiles to wasm
    // import 'dart:html' as html;
    // import 'dart:js_util' as js_util;
    // html.document.getElementById('root')!.classes.add('ready');
    // js_util.callMethod(js_util.globalThis, 'track', ['open']);
    …
  10. 10

    The marketing site is React and they want your Flutter chart on the page with data flowing both ways — how do you mount it and talk across the boundary?

    Hard

    Flutter web mounts into whatever element you hand the loader through hostElement, and the two sides exchange data only through explicit JS interop — there is no shared state, no shared router and no shared event bus.

    // web/flutter_bootstrap.js — your copy of the template, mounting into #chart:
    //   {{flutter_js}}
    //   {{flutter_build_config}}
    //   _flutter.loader.load({
    //     config: { hostElement: document.querySelector('#chart') },
    //   });
    …
  11. 11

    The desktop build opens at the runner's default size, has no real menu bar, and Cmd+Q throws away unsaved work — what is framework and what is a plugin?

    Hard

    Almost none of the window is Dart: the first window is created by the native runner checked into your own repo, menus and exit handling are framework APIs, and everything you want to change at runtime is a package.

    // The first window exists before Dart runs — edit the runner, not main():
    //   windows/runner/main.cpp    Win32Window::Size size(1280, 800);
    //   macos/Runner/MainFlutterWindow.swift   self.setFrame(frame, display: true)
    //   linux/runner/my_application.cc  gtk_window_set_default_size(window, 1280, 800);
    
    import 'package:flutter/material.dart';
    …
  12. 12

    Export to CSV writes the file on macOS and does nothing at all in the web build — where exactly does the browser stop you?

    Hard

    There is no filesystem on the web: dart:io and path_provider do not exist there, so a save is a download and every read starts from a file the user personally handed you in a gesture.

    import 'dart:convert';
    import 'dart:js_interop';
    import 'package:file_selector/file_selector.dart';
    import 'package:flutter/services.dart';
    import 'package:web/web.dart' as web;
    …
  13. 13

    Ctrl+S saves from anywhere, but dies as soon as a dialog opens and fires while the user types in the search field — how is that key actually routed?

    Hard

    A key event goes from HardwareKeyboard to the primary focus and then up the focus chain: the first Shortcuts on that path that matches turns it into an Intent, and the first enabled Action for that Intent type above it runs — so both bugs are about where your Shortcuts sits relative to focus.

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    class SaveIntent extends Intent {
      const SaveIntent();
    }
    …
  14. 14

    The app ships on web next month and pubspec has forty dependencies — how do you find the ones with no web implementation before QA does?

    Hard

    Audit platform support as data before the first build, because a missing implementation surfaces in three different places — a failed build, a MissingPluginException on the first call, or a silent no-op — and only the first one is loud.

    // CI, on every pull request — the gaps that fail a build are the cheap ones:
    //   flutter build web --release
    //   flutter build macos --release
    //   flutter pub deps --style=compact   # then check each plugin's platforms
    
    import 'package:flutter/foundation.dart';
    …
  15. 15

    You have to ship the desktop app outside any store and it must update itself — what does each platform demand before a user can double-click it?

    Hard

    Each OS wants its own signature and its own package: a signed MSIX or installer on Windows, a Developer ID build with the hardened runtime that is notarised and stapled on macOS, and a distro package or Flatpak on Linux — and the updater is entirely yours to build.

    # --- Windows ---------------------------------------------------------------
    flutter build windows --release
    # ship all of build/windows/x64/runner/Release/ : exe + DLLs + data/
    dart run msix:create --release    # reads msix_config: from pubspec.yaml
    signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 \
      /f acme.pfx /p "$CERT_PASSWORD" build/windows/x64/runner/Release/Ledger.msix
    …
  16. 16

    Every request from the web build fails with "XMLHttpRequest error" and no status code while iOS is fine — how do you debug that in a release web build?

    Hard

    That message is the browser refusing to let Dart see the response, and it is almost always CORS — the exception carries no status because your code never received one.

    // Build with maps, then serve it the way the host serves it:
    //   flutter build web --release --source-maps --base-href /app/
    //   dart pub global run dhttpd --path build/web --port 8080
    //   # devtools -> Application -> Service Workers -> Unregister, then hard reload
    //   # a stale worker serves yesterday's main.dart.js and hides your fix
    …