Irbisa · Cheatsheet

Flutter on Web & Desktop

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

Senior Developer · 20 questions

  1. 01

    How do you add web or desktop support to an existing Flutter project, and what does that generate?

    Run flutter create --platforms=web,macos,windows,linux . in the project root.

    # from the project root; lib/ is not touchedflutter create --platforms=web,macos,windows,linux . flutter doctor        # Xcode / Visual Studio / GTK toolchainsflutter devices       # chrome, macos, ...flutter run -d chrome…
  2. 02

    What are the limitations of Flutter Web, and when is it a good fit?

    Flutter web paints the whole UI into a canvas, so it fits app-like products behind a login (dashboards, internal tools, editors, a web companion to a mobile app) and fits badly where a page must load instantly, be indexed or behave like a document.

  3. 03

    Which renderers does Flutter web use today (CanvasKit or skwasm), and what happened to the HTML renderer?

    Two, both Skia compiled to WebAssembly: CanvasKit for the default JavaScript build and skwasm for the --wasm build.

  4. 04

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

    Because --wasm is opt-in, and even then the output holds a JavaScript build beside the wasm one: flutter.js picks per browser, so every user whose browser cannot run the wasm build still gets JS.

    # Emits main.dart.wasm AND main.dart.js; flutter.js picks per browserflutter build web --release --wasm # Multi-threaded skwasm needs cross-origin isolation on the HTML documentcurl -sI https://app.example.com/ | grep -i '^cross-origin'# Cross-Origin-Embedder-Policy: credentialless…
  5. 05

    Users stare at a white page for two seconds before the web app appears. What runs before the first Flutter frame, and how do you shorten it?

    The HTML loads, flutter_bootstrap.js picks a build, main.dart.js and the renderer download and compile, the engine initialises, and only then does runApp paint.

    // index.html: <div id="boot">skeleton markup</div> and//   window.addEventListener('flutter-first-frame', () =>//       document.getElementById('boot')?.remove()); // before: every await here is white screen//   await Firebase.initializeApp(); await config.fetch(); runApp(...);…
  6. 06

    The admin console is imported with deferred as, yet main.dart.js barely shrank and no part file appeared. Why?

    // main.dartimport 'admin/admin_page.dart' deferred as admin;import 'routes.dart'; Future<Widget> openAdmin() async {  await admin.loadLibrary();  return admin.AdminPage();} // routes.dart: imported eagerly by main.dartimport 'package:flutter/material.dart';import 'admin/admin_page.dart'; final routes = <String, WidgetBuilder>{  '/': (_) => const HomePage(),  '/admin': (_) => const AdminPage(),};

    dart2js moves a library into its own part file only when every path to it is deferred.

  7. 07

    Deployed to GitHub Pages under /my-app/, the web build shows a blank page and every asset 404s. What does --base-href fix, and what else changes?

    index.html resolves every relative URL against its <base href>, which defaults to /, so the app requests /main.dart.js instead of /my-app/main.dart.js.

    # GitHub Pages project site: https://user.github.io/my-app/flutter build web --release --base-href /my-app/ grep '<base' build/web/index.html# <base href="/my-app/">  (web/index.html must keep <base href="$FLUTTER_BASE_HREF">)…
  8. 08

    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?

    Nothing from inside the Flutter app.

  9. 09

    How do you make a Flutter web app an installable PWA, and make sure users actually get the new build after a deploy?

    The generated manifest.json and icons already make it installable; offline caching and updates are yours.

    # What browsers were told on the last deploycurl -sI https://app.example.com/main.dart.js | grep -i cache-control # nginx: revalidate the entrypoints, long caching only for files you version yourself#   location ~ ^/(index\.html|flutter_bootstrap\.js|main\.dart\.(js|mjs|wasm)|version\.json)$ {#     add_header Cache-Control "no-cache";…
  10. 10

    Every request from the web build fails with "XMLHttpRequest error" or "Failed to fetch" and no status code, while iOS works. What is going on?

    The browser blocked the response before Dart could see it (almost always CORS, sometimes mixed content).

  11. 11

    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?

    dart:html, dart:js, dart:js_util and package:js are the legacy interop and do not exist for dart2wasm.

    import 'dart:js_interop';import 'dart:js_interop_unsafe';import 'package:web/web.dart' as web; // before (not available to dart2wasm)://   html.document.getElementById('root')!.classes.add('ready');…
  12. 12

    This conditional import works in the JavaScript build, but under --wasm the app silently runs the stub. Why?

    // storage.dartimport 'storage_stub.dart'    if (dart.library.io) 'storage_io.dart'    if (dart.library.html) 'storage_web.dart'; // storage_web.dartimport 'package:web/web.dart' as web; String? readToken() => web.window.localStorage.getItem('token'); // storage_stub.dartString? readToken() => null;

    The condition asks whether the compilation target has dart:html, and dart2wasm does not have it.

  13. 13

    Find the bug: the editor works on Android, iOS and macOS, but in the web release build the toolbar is just a grey box.

    import 'dart:io' show Platform;import 'package:flutter/material.dart'; class EditorToolbar extends StatelessWidget {  const EditorToolbar({super.key, required this.onSave});  final VoidCallback onSave;   @override  Widget build(BuildContext context) {    final desktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;    return Row(children: [      IconButton(icon: const Icon(Icons.save), onPressed: onSave),      if (desktop)        Text(Platform.isMacOS ? 'Cmd+S' : 'Ctrl+S'),    ]);  }}

    Platform comes from dart:io, which the web compilers accept but do not implement: Platform.isMacOS throws Unsupported operation: Platform._operatingSystem inside build, and a release build paints the grey ErrorWidget instead of the toolbar.

  14. 14

    The same screen scrolls smoothly on a phone but janks in the browser. Why is Flutter web slower, and how do you find and fix the cost?

    In the browser your Dart runs as JavaScript or wasm on the page's main thread, and with CanvasKit the rasterizing happens there too, so build, layout, paint and raster share one thread that mobile splits in two.

  15. 15

    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?

    Flutter web mounts into whatever element you pass the loader as hostElement, and the two sides exchange data only through explicit JS interop: no shared state, router or event bus.

    // web/flutter_bootstrap.js (your copy of the template)://   {{flutter_js}}//   {{flutter_build_config}}//   _flutter.loader.load({ config: { hostElement: document.querySelector('#chart') } });// React side: <div id="chart" style={{height: 360}} />//   el.addEventListener('chart-selection', (e) => setSelected(e.detail.id));…
  16. 16

    Find the bug: after opening and closing the payment screen a few times, each postMessage is handled several times and setState() called after dispose() appears.

    class _PaymentFrameState extends State<PaymentFrame> {  String _status = 'waiting';  void _onMessage(web.MessageEvent e) {    if (e.origin == payOrigin) setState(() => _status = '${e.data.dartify()}');  }  @override  void initState() {    super.initState();    web.window.addEventListener('message', _onMessage.toJS);  }  @override  void dispose() {    web.window.removeEventListener('message', _onMessage.toJS);    super.dispose();  }}

    Every .toJS call wraps the tear-off in a new JS function, so removeEventListener gets a function the window never saw and removes nothing.

  17. 17

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

    There is no filesystem on the web.

    import 'dart:convert';import 'dart:js_interop';import 'package:file_selector/file_selector.dart';import 'package:web/web.dart' as web; // wrong on web: compiles, then fails; there is no documents directory…
  18. 18

    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?

    Audit platform support as data before the first web build, because a missing implementation shows up three ways (a failed build, a MissingPluginException on the first call, or a silent no-op), and only the first one is loud.

  19. 19

    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?

    Very little of the window is Dart.

    import 'dart:ui' show AppExitResponse; // initial size lives in the native runner, not hereclass _EditorState extends State<Editor> {  late final AppLifecycleListener _lifecycle;…
  20. 20

    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?

    Sign and package per OS: a signed MSIX or installer on Windows, Developer ID signing plus notarization on macOS, a bundle built on the oldest glibc and wrapped as .deb, Flatpak or AppImage on Linux.

    # --- 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 # --- macOS -----------------------------------------------------------------…