Skip to content
Blog

Offline Support in Flutter Web: Service Workers the Modern Way

How offline support works in Flutter web today, from the legacy caching service worker to a Workbox setup you own and control.

Published on • November 10, 2026

AI Assistant

Offline support in Flutter web has moved under your control. Older Flutter versions generated a caching service worker for you, while current versions write only a self-cleaning stub and expect you to configure your own caching. This post explains what changed, what the legacy worker actually did, and how to build a modern offline experience with Workbox and Flutter’s precache manifest.

What changed: from generated service worker to a stub

For years, flutter build web emitted a flutter_service_worker.js that registered itself and cached the entire app. That file is gone as a managed feature: Flutter no longer generates or manages a caching service worker, and it writes a small stub into the build output that cleans up leftover caches from earlier Flutter versions rather than caching anything new.

You still get one important piece of build output: with Flutter 3.50 and later, flutter build web --web-content-hash emits a precache_manifest.json in build/web/. Each entry contains the url, a content hash, a size, and a urlHashed flag, which is exactly the data a precaching step needs. The build keeps all generated files in sync, so your own service worker can consume this manifest instead of hard-coding a file list.

# Emit build/web/precache_manifest.json alongside the app
flutter build web --release --web-content-hash

The legacy worker, for maintenance of old projects

If you maintain a project built with an older Flutter, it is worth knowing what flutter_service_worker.js did, because your replacement needs to do the same job. The file was a plain script with three constants: MANIFEST='flutter-app-manifest', TEMP='flutter-temp-cache', and CACHE_NAME='flutter-app-cache'. It embedded a RESOURCES map of every build file to an MD5 hash, registered a fetch handler that served cache-first and re-fetched when the hash changed, and deleted cache entries that were no longer in the manifest. Registration happened on the first frame via navigator.serviceWorker.register('flutter_service_worker.js'), and builds defaulted to the offline-first strategy, with --pwa-strategy none to opt out.

// Legacy registration snippet, for old Flutter builds
if ('serviceWorker' in navigator) {
  window.addEventListener('flutter-first-frame', function () {
    navigator.serviceWorker.register('flutter_service_worker.js');
  });
}

Keep this file only while you are still serving an old build; a new build will not regenerate it.

Service worker fundamentals you now own

A service worker is a JavaScript file that runs in a browser thread separate from the page, giving it control over network requests for its scope. It responds to lifecycle events: install fires when the worker is first registered or when the file byte-differs from the installed one, activate fires when it takes over the page, and fetch fires for every request the page makes while the worker controls it. The usual pattern for offline-first apps is to precache assets in install, call self.skipWaiting() to skip the waiting phase, self.clients.claim() to take over open pages immediately, and clean old caches in activate.

// web/sw.js - minimal offline-first worker using the Flutter manifest
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';

self.skipWaiting();
self.clients.claim();

// Flutter 3.50+ writes build/web/precache_manifest.json
const manifest = self.__WB_MANIFEST || [];
precacheAndRoute(manifest);
cleanupOutdatedCaches();

The Cache API itself is small: caches.open(name) returns a promise for a Cache, cache.addAll(urls) fetches and stores a list of responses, cache.match(request) resolves with the cached response or undefined, and cache.put(request, response) stores one. A service worker only runs in a secure context, so HTTPS or localhost, and it is unavailable in Firefox private browsing because caches cannot be accessed there.

Wiring the manifest into Workbox

Workbox is Google’s set of libraries for precaching and runtime caching, and it is the approach Flutter’s current docs recommend for configuring your own worker. The precaching module expects entries of the shape {url, revision}; Flutter’s manifest gives you hash and urlHashed, so map revision to entry.urlHashed ? null : entry.hash and you get cache-busting by URL when the filename is content-hashed and by revision otherwise.

In practice you have three build paths for generating the manifest: workbox-build as a Node script, the Workbox CLI, or a bundler plugin for webpack-based builds. A simple Node script post-processing the Flutter output looks like this:

# After flutter build web --web-content-hash
node build-sw.mjs   # merges build/web/precache_manifest.json into web/sw.js

At runtime, workbox-precaching serves cache-first from the precache, so a repeat visit renders without touching the network, and cleanupOutdatedCaches() removes entries from previous deployments during activate.

Registering your worker from the Flutter app

Registration belongs in web/index.html rather than in Dart, so it starts as early as possible:

<!-- web/index.html -->
<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
      navigator.serviceWorker.register('sw.js').catch(console.error);
    });
  }
</script>

For custom or sensitive data, do not rely on precaching: create your own cache with caches.open('app-data') and store explicit responses with cache.put(), then decide per request whether to serve cache-first, network-first, or stale-while-revalidate from the fetch handler.

Verify offline behavior

Test the real thing rather than trusting the console. Serve your build over localhost, load the app once, then in Chrome DevTools switch to Offline mode under the Network tab and reload: the app should still boot from the service worker cache. Check the Application tab to confirm the service worker is “activated and running”, the precache is populated, and no requests fall through to the network. Repeat after a second deploy to confirm old caches were cleaned up and the new manifest version is what gets served.

Takeaway: Flutter web no longer ships a caching service worker, so own the offline story yourself: generate precache_manifest.json with --web-content-hash, feed it to Workbox precacheAndRoute, and register the worker in index.html.

Sources