The PWA Wiki logo

PWA Wiki

Service worker

Getting started with Workbox

Include this inside your Service Worker file (e.g. sw.js)


if (workbox) {
  console.log(`Yay! Workbox is loaded 🎉`);
} else {
  console.log(`Boo! Workbox didn't load 😬`);


Workbox Precaching

workbox.precaching.precacheAndRoute(['/', 'index.html'], 'GET');

Workbox Caching Strategies

Default Cache Strategy is CacheFirst
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'css_js'

Workbox Preloading Routes


/* app.js */

window.addEventListener('load', async () => {
    if ('serviceWorker' in navigator) {
        const registration = await navigator.serviceWorker.register('./service-worker.js', { scope: SERVICE_WORKER_SCOPE });


By default, a page's fetches won't go through a service worker unless the page request itself went through a service worker. So you'll need to refresh the page to see the effects of the service worker. clients.claim() can override this default, and take control of non-controlled pages

Install event

self.addEventListener('install', function(event) {
  event.waitUntil( {
      return cache.addAll(

Activate event

self.addEventListener('activate', async event => {

  await self.registration.navigationPreload.enable();

    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to remove this cache,
          // but remember that caches are shared across
          // the whole origin
        }).map(function(cacheName) {
          return caches.delete(cacheName);

Fetch event

// when the browser requests a resource
addEventListener('fetch', event => {
        // look in the cache for the resource
        caches.match(event.request).then(async response => {
            if (response) {
                // is in cache, respond with the cached resource
                return response;
            // if not found fetch it from the network
            const networkResponse = await fetch(event.request);
            // response needs to be cloned if going to be used more than once
            const clonedResponse = networkResponse.clone();
            // save response to runtime cache for later use
            const runtimeCache = await'runtime-cache');
            runtimeCache.put(event.request, networkResponse);
            // respond with the cloned network response
            return Promise.resolve(clonedResponse);


Default offline page



const OFFLINE_HTML = '/offline.html';
const PRECACHE = [


const htmlHandler = new workbox.strategies.NetworkOnly();
// A NavigationRoute matches navigation requests in the browser, i.e. requests for HTML.
const navigationRoute = new workbox.routing.NavigationRoute(({ event }) => {
  const request = event.request;
  return htmlHandler.handle({ event, request }).catch(() => caches.match(OFFLINE_HTML, {
    ignoreSearch: true

Vanilla JS

addEventListener('fetch', async event => {
    if (event.request.mode === 'navigate') {
      event.respondWith((async () => {
        const staleWhileRevalidate = new workbox.strategies.StaleWhileRevalidate();
        const url = event.request.url;
        try {
          // check if the event.request.url exists in the cache or in the network
          const response = await caches.match(event.request) || await fetch(event.request);
          if (!response || response.status === 404) {
            throw new Error(response.status);
          } else {
            return await staleWhileRevalidate.handle({event});
        } catch (error) {
          // if not found in any of the two, respond with the offline.html file
          console.warn(`ServiceWorker: ${url} was not found either in the network or the cache. Responding with offline page instead.\n`);
          return await caches.match(OFFLINE_PAGE_URL) || await fetch(OFFLINE_PAGE_URL, { method: 'GET' });

Mark offline unavailable pages

  1. Register a route for pages to cache - service-worker.js
    // * store projects' media in a different runtime cache
    // * checking later inside this cache to decide which projects are available offline
      new workbox.strategies.CacheFirst({
        cacheName: 'runtime-pages-media'
  1. Mark pages when offline - app.js
    1. Check offline status
      • on startup / window load
      if (!navigator.connection.downlink) {
      	// is offline
      • on change of network conditions
      window.addEventListener('offline', markOfflineUnavailablePages);
      window.addEventListener('online', unmarkOfflineUnavailablePages);
    1. Handle offline event
      const markOfflineUnavailableProjects = () => {
        const pages = document.querySelectorAll('');
     'runtime-pages-media').then(pagesRuntimeCache => {
          pages.forEach(page => {
            pagesRuntimeCache.keys().then(keys => {
              const cachedPage = keys.find(key => key.url === page.href);
              if (!cachedPage) {
                // mark page as unavailable while offline
              } else {
                // add the page to a list of offline available pages
  1. Add styling - styles.css
    .unavailable-offline {
      opacity: 0.25; 
      pointer-events: none; 

Push notifications

Push examples website

Glitch Repo

Background Sync

Vanilla JavaScript

Replaying offline requests example website


Glitch repo

Placeholder image fallback

const placeholderImageURL = '/img/placeholder-image.png'; // precache this in __precacheManifest file
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);

  async ({url, event, params}) => {
    const staleWhileRevalidate = new workbox.strategies.StaleWhileRevalidate();

    try {
      const response = await caches.match(event.request) || await fetch(url, { method: 'GET' });
      if (!response || response.status === 404) {
        throw new Error(response.status);
      } else {
        return await staleWhileRevalidate.handle({event});

    } catch (error) {
      console.warn(`\nServiceWorker: Image [${url.href}] was not found either in the network or the cache. Responding with placeholder image instead.\n`);
      // * get placeholder image from cache || get placeholder image from network
      return await caches.match(placeholderImageURL) || await fetch(placeholderImageURL, { method: 'GET' });


Offer reload on new version

Custom pull-to-refresh effect

Install - Add to Home screen

Install button


  1. Install npm package: npm install @pwabuilder/pwainstall
  1. Import inside your app scripts: import '@pwabuilder/pwainstall'


  1. Include module in index.html:

Then you can use the element <pwa-install></pwa-install> inside your html files.

Custom implementation

Register service worker

if ('serviceWorker' in navigator) {
		.register('/sw.js', { scope: '/' })
	  .then(registration => {

Catching Install event

let deferredPromptEvent;

window.addEventListener('beforeinstallprompt', function(e) {
  deferredPromptEvent = e; = 'block';

Install button event listener

const installButton = document.getElementById('install-button');

installButton.addEventListener('click', function() { = 'none';

  deferredPromptEvent.userChoice.then(function(choiceResult) {
    console.log(choiceResult.outcome) // 'dismissed' or 'accepted'
      deferredPromptEvent = null;

Install banner for iOS (≤ 13)

// Detects if device is an iOS (including iOS 13) 
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);

// Detects if device is in standalone mode
const isInStandaloneMode = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;

// Checks if should display install popup notification:
if (isIos && !isInStandaloneMode) {
  this.setState({ showInstallMessage: true });

Web Manifest

Fullscreen mode on iOS

<meta name=”apple-mobile-web-app-capable” content=”yes”>


Icon Generators

Favicons - Maskable icon

  "icons": [
      "src": "./favicon/favicon-16x16.png",
      "sizes": "16x16",
      "type": "image/png"
      "src": "./favicon/favicon-32x32.png",
      "sizes": "32x32",
      "type": "image/png"
      "src": "./favicon/favicon-64x64.png",
      "sizes": "64x64",
      "type": "image/png"
      "src": "./favicon/android-chrome-192x192.png", 
      "sizes": "192x192", 
      "type": "image/png" 
      "src": "./favicon/android-chrome-512x512.png", 
      "sizes": "512x512", 
      "type": "image/png" 
      "src": "./favicon/maskable_icon.png",
      "sizes": "196x196",
      "type": "image/png",
      "purpose": "maskable"


<meta name="msapplication-square70x70logo" content="icon_smalltile.png">
<meta name="msapplication-square150x150logo" content="icon_mediumtile.png">
<meta name="msapplication-wide310x150logo" content="icon_widetile.png">


<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Example PWA">
<link href="icon-152x152.png" rel="apple-touch-icon">

iOS Splash screen

<link href="/apple_splash_2048.png" sizes="2048x2732" rel="apple-touch-startup-image" />
<link href="/apple_splash_1668.png" sizes="1668x2224" rel="apple-touch-startup-image" />
<link href="/apple_splash_1536.png" sizes="1536x2048" rel="apple-touch-startup-image" />
<link href="/apple_splash_1125.png" sizes="1125x2436" rel="apple-touch-startup-image" />
<link href="/apple_splash_1242.png" sizes="1242x2208" rel="apple-touch-startup-image" />
<link href="/apple_splash_750.png" sizes="750x1334" rel="apple-touch-startup-image" />
<link href="/apple_splash_640.png" sizes="640x1136" rel="apple-touch-startup-image" />

Using system fonts


<!-- ! Download Google fonts only if launched in the browser -> Else use system fonts -->
<link href="" rel="stylesheet" media="all and (display-mode: browser)" />


/* iOS */
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;

/* Android */
font-family: 'RobotoRegular', 'Droid Sans', sans-serif;

/* Windows Phone */
font-family: 'Segoe UI', Segoe, Tahoma, Geneva, sans-serif;

Github example

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";

Web Share API

HTML Streaming

Presentation by Philip Walton

  • Creating an app shell
  • Loading different parts of the website separately (navigation/app shell from the cache, content from server from NetworkFirst)

Navigation Preload





APK Generators


Web Perception API

Example and getting started guide

Tools & Resources

PWA Starter Kit

PWA Builder CLI

PWA and isomorphic rendering (SSR)

Progressive Web Components

PWA Technical Overview

Permissions site


Workbox v5

In v5, the glob-related configuration options are no longer supported. The webpack asset pipeline is the source of all the automatically created manifest entries. (Developers who have files that exist outside of the webpack asset pipeline are encouraged to use, e.g., copy-webpack-plugin to get those files into the webpack compilation.)

Reading pre-cached files directly

it's still a best practice to call getCacheKeyForURL() to determine the actual cache key, including the __WB_REVISION parameter, if you need to access precached entries using the Cache Storage API directly.

import {cacheNames} from 'workbox-core';
import {getCacheKeyForURL} from 'workbox-precaching';

const cache = await;
const response = await cache.match(

Alternatively, if all you need is the precached Response object, you can call matchPrecache(), which will automatically use the correct cache key and search in the correct cache:

import {matchPrecache} from 'workbox-precaching';

const response = await matchPrecache('/precached-file.html');

UI Fixes

Scroll into view when keyboard is shown on mobile


Common issues

Workbox new version


Serving Cached Video files on Safari (Range requests)

Common Questions