Service Workers for Offline Browser Games

high rise buildings during night time

The original Chrome Dino game exists because someone at Google thought a network error page deserved a hidden distraction. It is, by accident, the most famous offline browser game in history. Service workers let you build the same kind of offline-first experience for any browser game — playable without internet, installable to the home screen, reliable even when the network drops mid-session. This guide on service workers for offline games covers the caching strategies, the PWA model, and the practical pitfalls.

Key takeaways

  • Service workers are scripts that sit between your page and the network, letting you intercept and cache requests for offline use.
  • The cache-first strategy is the default for browser games — serve from cache, fall back to network.
  • A complete offline-capable game ships its HTML, JS, assets, and audio behind a service worker cache.
  • Adding a manifest.json and HTTPS lets users install the game to their home screen as a PWA.
  • Version bumps are the most common bug source — invalidate the cache on every release or new assets will silently fail.

What a service worker actually does

A service worker is a background script that the browser runs separately from the page. It can intercept network requests (via the `fetch` event), respond from a cache, and persist data through the Cache API and IndexedDB. It only runs over HTTPS (or localhost during development) and lives at a specific URL scope.

For games, the service worker is the offline cache layer. You register it once on first page load; it caches your game files; on subsequent visits, the page loads from cache without touching the network. The MDN Service Worker API documentation covers the full interface.

The caching strategies that matter

Service worker caching breaks down into a handful of well-known strategies. Three matter most for games.

Cache-first

The default for games. Try the cache. If the asset is there, serve it. Otherwise hit the network and cache the response. This minimizes network use and makes the game load fast even on flaky connections.

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached =>
      cached || fetch(event.request).then(response => {
        caches.open('game-v1').then(cache =>
          cache.put(event.request, response.clone())
        );
        return response;
      })
    )
  );
});

That’s the entire offline cache logic for a typical browser game. Twenty lines of code and the game works offline.

Network-first

Try the network. If it succeeds, serve and cache. If it fails, fall back to the cache. Better for content that needs to be fresh (leaderboards, daily challenges). Slower on flaky connections because the network attempt has to time out before the cache fallback runs.

Stale-while-revalidate

Serve from cache immediately, then update the cache from the network in the background. The user sees the cached version instantly; the next page load gets the fresh version. This is the right strategy for assets that rarely change but should eventually update.

The install lifecycle

A service worker registers, installs, and activates in a specific lifecycle. On first page load, the page calls `navigator.serviceWorker.register(‘/sw.js’)`. The browser downloads the script and fires `install`. Inside the install handler, you pre-cache the assets your game needs to run offline.

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('game-v1').then(cache => cache.addAll([
      '/',
      '/index.html',
      '/game.js',
      '/assets/sprites.png',
      '/assets/jump.mp3'
    ]))
  );
});

The `cache.addAll` call downloads every listed URL and stores it in the cache named `game-v1`. After install, the worker activates and starts intercepting fetch requests.

Versioning and cache invalidation

The most common service worker bug is stale caches. You ship a new version of the game, the user visits the site, the service worker serves the old cached version, and the user never sees the update.

The fix is versioning the cache name. Bump the version string on every release. In the `activate` handler, delete old caches.

const CACHE = 'game-v3';

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(names =>
      Promise.all(names
        .filter(name => name !== CACHE)
        .map(name => caches.delete(name))
      )
    )
  );
});

Pair this with `self.skipWaiting()` in install and `clients.claim()` in activate so the new worker takes over immediately rather than waiting for the user to close every tab. Without that, users can run an old worker for days.

The PWA model for browser games

A Progressive Web App is, at minimum, a website that has a service worker, runs over HTTPS, and ships a `manifest.json` describing the app. Users can install it to their home screen or app drawer. On install, the browser saves the app’s icon and launches it in a chrome-less window.

The minimum manifest for a browser game:

{
  "name": "My Browser Game",
  "short_name": "MyGame",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#ff5500",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

Link it from your HTML with `<link rel=”manifest” href=”/manifest.json”>`. Chrome and Edge will prompt installable visitors after a few engagement signals (return visit, time on site). On Android, the install option shows up in the browser menu. On iOS, users have to add to home screen manually.

What to cache for an offline game

The pre-cache list for a typical browser game:

  • The HTML file (or `/` if your game serves an index)
  • All JavaScript bundles
  • CSS files
  • Sprite atlases and texture files
  • Audio files (sound effects and music)
  • Font files
  • The PWA manifest and icons

Total size for a typical small browser game: 500 KB to 5 MB. The cache storage quota is generous on most platforms (often a percentage of available disk), so even 50 MB games cache fine. The constraint is download time on first visit, not storage.

Saving game state offline

The cache handles assets. Save data is a separate concern. Use IndexedDB for structured game state — high scores, level progress, settings, user-created content. localStorage works for trivially small data (a few flags) but is synchronous and limited to ~5 MB.

For most browser games, save data is small enough that localStorage is fine. For games with substantial user data (drawn levels, photo imports, ROM saves), IndexedDB scales better.

Network detection and graceful degradation

An offline-capable game should detect network status and behave appropriately. The `navigator.onLine` property gives a basic signal; `window.addEventListener(‘online’/’offline’)` fires when status changes.

Practical uses:

  • Hide multiplayer matchmaking when offline.
  • Queue analytics events to send when the connection returns.
  • Show a small offline indicator so users know the cause if multiplayer doesn’t work.
  • Switch to a local-only leaderboard when global submission isn’t possible.

Don’t make the game itself stop working when offline — that defeats the point of caching it. The offline experience should be the default, with online features layered on top.

Testing offline behavior

Chrome DevTools’ Application tab shows the service worker status, cache contents, and lets you simulate offline mode. The Network tab has an “Offline” toggle for testing. Use both during development.

The single most common bug surface is updating the service worker. Test the full update flow: load the page with version 1, deploy version 2, reload, and verify the new code actually runs. Then verify a fresh visitor gets version 2 directly. Then verify the cache deleted the v1 entries.

Limits and gotchas

  • HTTPS required. Service workers don’t run over plain HTTP. localhost is the exception for development.
  • Scope matters. A worker at `/games/sw.js` only controls pages under `/games/`. Place it at the root if you want site-wide control.
  • Browsers can evict caches. Under storage pressure, browsers can clear service worker caches. Don’t treat them as permanent storage. Use IndexedDB with explicit persistent storage requests for important user data.
  • iOS Safari is more restrictive. PWA installation on iOS has limitations vs Android and desktop. Test on real devices.

The compound benefit

An offline-capable game loads instantly on repeat visits, works on flaky transit Wi-Fi, and installs to the home screen for users who want a native-app feel. The total work is roughly an afternoon of setup plus a small ongoing tax on every release (bump the cache version).

For comparison, the original Chrome offline page game became iconic partly because the offline experience itself felt like a small gift. Service workers let you give the same gift to anyone visiting your browser game. For more about games that thrive without internet, see our piece on browser games with no internet required.

Frequently asked questions

What is a service worker?

A service worker is a background script that sits between a web page and the network. It can intercept requests, respond from a cache, and run when the page isn’t open. For games, it’s the offline cache layer that makes the game playable without internet.

Which caching strategy should I use for games?

Cache-first for game assets — try the cache, fall back to the network. This minimizes network use and makes the game load instantly on repeat visits. Network-first is better for fresh content like leaderboards.

How do I update a service worker?

Bump the cache name on every release (e.g. `game-v1` to `game-v2`). In the activate handler, delete old caches. Call `self.skipWaiting()` and `clients.claim()` so the new worker takes over immediately.

What’s the difference between localStorage and IndexedDB for save data?

localStorage is synchronous and limited to ~5 MB; it’s fine for small flags and settings. IndexedDB is asynchronous and scales to large structured data; it’s the right choice for substantial save data, level editors, and user-generated content.

Do I need a service worker to make a PWA?

Yes. A Progressive Web App requires at minimum a service worker, HTTPS, and a manifest.json. Without a service worker the page can’t be installed or run offline.

The takeaway

Service workers turn a browser game into an offline-capable, installable app for the cost of a small JavaScript file. The default cache-first strategy is twenty lines of code and works for almost every game. Add a manifest, ship over HTTPS, and your game lives on the home screen like a native app — without the store review or install friction. The pattern is well-established, the browser support is universal, and the ongoing maintenance cost is essentially bumping a version string. There’s no reason a serious browser game shouldn’t have one.