How I built my first Progressive Web App (PWA)

Category: Blog

Tagged: pwa

Published at:

As of today, my site is available as a Progressive Web App. Yaay! 💯

In this post, you would learn more about how and why I added this exciting feature to my site.

Why did I do it

I like to think of myself as a very pedantic person, and I was trying to make my site better from the very first day. At the time when I was building my site, I didn’t understand all metrics from web performance tools like PageSpeed Insights or WebPageTest. But, as I was trying to make my site better, I was learning new techniques, and my website got better and better.

Now my site gets top scores, but one thing was bothering me for some time. It was the Progressive Web App score.

Lighthouse score for Progressive Web App before optimisation.

By looking at the PWA report, I realised my site is ready for PWA. There were only a few issues to resolve. I didn’t understand these issues, but that never stopped me before.

How I did it

As my starting point, I decided to follow the “Your First Progressive Web App” tutorial. The very first step was to update my webmanifest.json file. I have added start_url and display options and some required meta tags, like <meta name="apple-mobile-web-app-capable" content="yes">.

{
  "name": "SB site - Silvestar Bistrović website",
  "short_name": "SB site - Silvestar Bistrović website",
  "icons": [],
  "theme_color": "#12e09f",
  "background_color": "#fff",
  "start_url": "/offline.html",
  "display": "standalone"
}

Next, I created sw.js file for Service Worker. To register service worker, there is a small snippet that needs to be added to your index page:

// CODELAB: Register service worker.
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then((reg) => {
        console.log('Service worker registered.', reg);
      });
  });
}

The content of the Service Worker file could vary, depending what you want to use achieve with your site. Since my site is quite straightforward, I decided to make use of basic offline experience only. That means I needed an offline.html file for offline experience. It would be a stripped version of my homepage. So I removed external images, and created a placeholder using CSS. I removed external CSS file and inlined it in head section. The only thing left to do was to add favicon files. I am not yet sure if this is need, but I decided to put it there, just in case. Those files aren’t big anyway.

The sw.js file could be broken into four segments:

  • defining constants,
  • installation,
  • activation, and
  • fetching.

First, I defined the cache name and which files to cache.

// constants
const CACHE_NAME = 'sb-cache-v1.3'
const FILES_TO_CACHE = [
  '/offline.html',
  '/favicon/apple-touch-icon.png',
  '/favicon/favicon-32x32.png',
  '/favicon/favicon-16x16.png',
  '/favicon/site.webmanifest',
  '/favicon/safari-pinned-tab.svg',
  '/favicon/favicon.ico',
  '/favicon/mstile-144x144.png',
  '/favicon/browserconfig.xml'
]

Next, I created the install event which opens cache with given cache name and caches the files.

self.addEventListener('install', (event) => {
  // CODELAB: Precache static resources here.
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('[ServiceWorker] Pre-caching offline page')
      return cache.addAll(FILES_TO_CACHE)
    })
  )
})

After that, I created the activate event, which cleans cached files from disk.

self.addEventListener('activate', (event) => {
  // CODELAB: Remove previous cached data from disk.
  event.waitUntil(
    caches.keys().then(keyList => Promise.all(keyList.map((key) => {
      if (key !== CACHE_NAME) {
        console.log('[ServiceWorker] Removing old cache', key)
        return caches.delete(key)
      }
    })))
  )
})

Finally, I created the fetch event, which handles page navigations only when request .mode is navigate. If the request fails to fetch the item from the network, it tries to fetch the offline.html file.

self.addEventListener('fetch', (event) => {
  // CODELAB: Add fetch event handler here.
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request)
        .catch(() => caches.open(CACHE_NAME)
          .then(cache => cache.match('offline.html')))
    )
  }
})

Final results

After the deployment, I run the audit for the site and now I have it looks like this:

Lighthouse score for Progressive Web App after optimisation.

I think fireworks deserve another appearance. 💯

Next steps

The next steps are to learn more about Workbox. And after that, I plan to add full offline experience for my side project Code Line Daily.

Edit: Continue reading my second article about “How I built my second Progressive Web App (PWA)”.