Building a Next JS PWA using next-pwa and service worker

August 23, 2021 by Deepak Bhadoriya
Building a Next JS PWA using next-pwa and service worker

Before jumping into building a Next JS PWA (progressive web app), let’s go over some basics.

What is a PWA?

PWA refers to 'Progressive web application'. PWAs are built using common web technologies like HTML, CSS, and JavaScript, but they feel almost like native apps. PWAs offer many of the same functionalities as native apps — like push notifications, offline support, hardware access, and much more. Meanwhile, native apps are built using a programming language specific to that platform — like Swift for iOS and Kotlin or Java for Android.

PWAs offer great flexibility over native apps. You can reuse your web application codebase to build a PWA. Many prominent companies have built and deployed their PWAs and seen significant growth in their business. One example is Twitter Lite — a PWA that delivers a state-of-the-art experience to Twitter users with lower data consumption, instant loading, and high user engagement. With this PWA, Twitter saw a 75% increase in tweets sent, 65% increase in pages per session, and a 20% decrease in bounce rate.

The Engineering Lead of Twitter Lite, Nicolas Gallagher said:

Twitter Lite is now the fastest, least expensive, and most reliable way to use Twitter. The web app rivals the performance of our native apps but requires less than 3% of the device storage space compared to Twitter for Android.

What is a Service Worker?

A service worker is a script that runs in the background in your browser on a separate thread, separate from a web page. This lets us use features like push notifications and background sync that don't require user or web page interaction. It sits between the network and the browser, acting as a proxy server. So using service workers, we can control network requests. Cache requests to enable offline access to cached content, which also improves performance! Note that this network cache is independent of the browser cache or network status. Also, this cache is persistent. As the Service worker is a JavaScript worker, we can’t access DOM directly in the service worker.

Now we have a good understanding of PWA and Service workers — why these technologies exist and why we should use them. With that, it's time to start converting an existing or new Next JS application into a PWA. Let's go!

Step 1: Install next-pwa

In the first step, we will install an npm package next-pwa in our project. Run:

npm install next-pwa

OR

yarn add next-pwa

Step 2: Create a Service worker for offline support and caching

We need to create a service-worker.js file in the root directory to add offline support and caching. We should also configure this for push notifications or any other features we might want to add in the future. Here is the example service worker —

import { skipWaiting, clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { NetworkOnly, NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { registerRoute, setDefaultHandler, setCatchHandler } from 'workbox-routing';
import { matchPrecache, precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';

skipWaiting();
clientsClaim();

// must include following lines when using inject manifest module from workbox
// https://developers.google.com/web/tools/workbox/guides/precache-files/workbox-build#add_an_injection_point
const WB_MANIFEST = self.__WB_MANIFEST;
// Precache fallback route and image
WB_MANIFEST.push({
  url: '/fallback',
  revision: '1234567890',
});
precacheAndRoute(WB_MANIFEST);

cleanupOutdatedCaches();
registerRoute(
  '/',
  new NetworkFirst({
    cacheName: 'start-url',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 1,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);
registerRoute(
  /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
  new CacheFirst({
    cacheName: 'google-fonts',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 4,
        maxAgeSeconds: 31536e3,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);
registerRoute(
  /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,
  new StaleWhileRevalidate({
    cacheName: 'static-font-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 4,
        maxAgeSeconds: 604800,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);
// disable image cache, so we could observe the placeholder image when offline
registerRoute(
  /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
  new NetworkOnly({
    cacheName: 'static-image-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 64,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);
registerRoute(
  /\.(?:js)$/i,
  new StaleWhileRevalidate({
    cacheName: 'static-js-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);
registerRoute(
  /\.(?:css|less)$/i,
  new StaleWhileRevalidate({
    cacheName: 'static-style-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);
registerRoute(
  /\.(?:json|xml|csv)$/i,
  new NetworkFirst({
    cacheName: 'static-data-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);
registerRoute(
  /\/api\/.*$/i,
  new NetworkFirst({
    cacheName: 'apis',
    networkTimeoutSeconds: 10,
    plugins: [
      new ExpirationPlugin({
        maxEntries: 16,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);
registerRoute(
  /.*/i,
  new NetworkFirst({
    cacheName: 'others',
    networkTimeoutSeconds: 10,
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

// following lines gives you control of the offline fallback strategies
// https://developers.google.com/web/tools/workbox/guides/advanced-recipes#comprehensive_fallbacks

// Use a stale-while-revalidate strategy for all other requests.
setDefaultHandler(new StaleWhileRevalidate());

// This "catch" handler is triggered when any of the other routes fail to
// generate a response.
setCatchHandler(({ event }) => {
  // The FALLBACK_URL entries must be added to the cache ahead of time, either
  // via runtime or precaching. If they are precached, then call
  // `matchPrecache(FALLBACK_URL)` (from the `workbox-precaching` package)
  // to get the response from the correct cache.
  //
  // Use event, request, and url to figure out how to respond.
  // One approach would be to use request.destination, see
  // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c
  switch (event.request.destination) {
    case 'document':
      // If using precached URLs:
      return matchPrecache('/fallback');
      // return caches.match('/fallback')
      break;
    case 'image':
      // If using precached URLs:
      return matchPrecache('/static/images/fallback.png');
      // return caches.match('/static/images/fallback.png')
      break;
    case 'font':
    // If using precached URLs:
    // return matchPrecache(FALLBACK_FONT_URL);
    // return caches.match('/static/fonts/fallback.otf')
    // break
    default:
      // If we don't have a fallback, just return an error response.
      return Response.error();
  }
});

Step 3: Create a fallback page when the app is offline

Due to service-worker.js, our app will still work when there is no network access available and serves the cached content. But what happens when a user visits a page that is not cached by a service worker? To handle this situation, we’ll create a fallback.jsx page in the pages directory which shows a message if the device is offline. Here is the example code for a fallback page —

import React from 'react';

const fallback = () => (
  <div>
    <h1>This is fallback page when device is offline </h1>
    <small>Route will fallback to this page</small>
  </div>
);

export default fallback;

Step 4: Create a next.config.js file

Now the next step is to create the next.config.js file inside the root directory. This will create an sw.js file using our service-worker.js file. This file will be served from the public folder in the production build. Here is a sample next.config.js file —

const withPWA = require('next-pwa');

module.exports = withPWA({
  pwa: {
    dest: 'public',
    swSrc: 'service-worker.js',
  },
});

Step 5: Provide all the information for the PWA

Now the next step is to create the manifest.json file inside the public directory and provide all the details related to the PWA like name, description, icon, etc. You can customize the name, icons, and other details according to your use case.

{
  "name": "Test PWA",
  "short_name": "Test PWA",
  "display": "standalone",
  "orientation": "portrait",
  "purpose": "any maskable",
  "theme_color": "#FFFFFF",
  "background_color": "#FFFFFF",
  "start_url": "/",
  "icons": [
    {
      "src": "/vercel.svg",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/vercel.svg",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Step 6: Link the manifest.json to the web application

In this step, we will use the manifest.json file that we created earlier and link that file to our web application in the HEAD section. Put this code in the HEAD section of your web application —

<link rel=”manifest” href=”/manifest.json” />

Step 7: Make a build and validate your PWA

This step is the last step of our PWA building process. Now all we have to do is make a build of our Next JS app! First, create a build using

npm run build

We are making the build because in dev mode there is a lot of unused JS code. So, we can't run the lighthouse audit in dev mode.

The output you see will be something like this —

pwa app cli outcome

Once the build has been created, start a server on localhost using

npm run start

Now you’ll start to see a small install icon in the URL bar of your browser. Go ahead and install your PWA! Once installed, this app will be added to your device’s app drawer with the icon that you selected in the manifest.json file. Now you can access the application directly by clicking on the app icon.

pwa app installation

Finally, you can validate your PWA using Lighthouse in dev tools. To do this, go to Chrome dev tools, open the Lighthouse tab, and click on Generate report. Once the report is ready, you’ll see something like this.

pwa light house report

That green mark above the ‘Progressive Web App’ label means that lighthouse validates our web application as a PWA.

And we’re done! I hope this post helped you understand the basics for building your first PWA in Next JS 😊

PWAProgressive web applicationNext JS PWAservice workernext-pwa

Got a project to discuss?