Avatar

Bonjour, I'm Julia.

Progressive Web App (PWA) in Next.js

#nextjs

6 min read

First things first - what even is a Progressive Web App, or PWA for short?

If you've got some time, check out the MDN docs to get a good overview. If I had to take a stab at defining PWAs in my own words, it's a way of allowing a web app to be accessed as if it was a native app, on mobile or desktop devices.

So, what is it that actually makes an app, a Progressive Web App?

Whilst I don't think there's an official standard on this, some of the key principles to consider are:

  • Is it installable, so that it can be accessed, say on your mobile home screen?
  • Does it work offline / under poor network connections?
  • Is it responsive to whatever device it's being viewed on?
  • Does it adhere to progressive enhancement, in that it caters to older devices (at a more basic level), but also newer ones?

Why might you want to consider making your web app into a PWA?

Whilst it can be simple enough to allow your users to continue accessing your app through your mobile browser, allowing it to be accessed as a pseudo native app directly from a home screen may:

  • Be more efficient;
    • Loading times can be near instantaneous due to the use of service workers and caching.
    • You only need to send updates for bits that have actually changed, as opposed to updating an entire native app.
  • Provide a nicer native experience vs. a web browser e.g. with mobile-specific app icons, responsive design and features and full screen modes);
  • Allow your users to access your app without a network connection; and
  • Provide more direct connection and engagement with your end user through use of features like push notifications.

How to create a PWA in Next.js

The really nice thing about Next.js is that they have an official way of doing this with the next-pwa package. Here are the steps I took:

  • Install the package with yarn add next-pwa.

  • If you don't already have one, create a manifest.json file in your /public folder. This file basically provides metadata about your app, to your browser, so that it knows how to render your app when downloaded as an extension on desktop or mobile. You can search for manifest generators online, but the one thing you'll need beforehand is an icon for your app. You can see an example of what my manifest file looks like in the appendix below.

  • We now need make the manifest.json file accessible on browser load. Within the /pages directory, open (or create) the _document.tsx file. If you've not come across this file before, read more about it here. You basically want to add a link to your manifest JSON within the <Head> tags so that your browser can access it. Alternatively, if you've already got access to your app's <Head> tags somewhere else (e.g. index.tsx), then just put it in there.

    <Head>
      // ...
      <link rel="manifest" href="/manifest.json" />
      // ...
    </Head>
  • In next.config.js, require the next-pwa package and wrap your module export function with it. (Detailed instructions are available in the next-pwa Github repo.)

    const withPWA = require('next-pwa')
    
    module.exports = withPWA({
      target: 'serverless',
      pwa: {
        dest: 'public',
        disable: process.env.NODE_ENV === 'development',
      },
      // ... whatever other config you might have
    })

    Note that there are other settings you can configure your PWA to adhere to, so just check out the official documentation to learn more.

And that's basically the setup in a nutshell. To test it's all working as expected, you can uncomment out the disable line for the dev environment in the config file above, then build your app in Next (I use yarn build for this). Head over to localhost:3000 and you should see an option in your browser to "install" your app.

This is what it looks like on my production site: How to install the PWA

Note that running this build for the first time will create a number of new PWA files in your public folder, including sw.js which is your service worker. You're going to want to ensure these don't get cached so remember to ignore these files in your version control (see troubleshooting tips below).

Offline support

One thing to note is that the next-pwa package tries to load content by grabbing it from the cache and network. If both of these sources fail, an error page will be rendered instead. For a nicer user experience, you can define a custom page for the user to see by creating a new file called _offline.tsx in the pages directory. All pages which cannot be downloaded will then display this page instead.

To overwrite the file location for this fallback page, in addition to setting up fallback content for other media types like images, videos, fonts etc., you can define these in the next.config.js file within the pwa settings.

module.exports = withPWA({
  target: 'serverless',
  pwa: {
    dest: 'public',
    disable: process.env.NODE_ENV === 'development',
    fallbacks: {
      image: '/offline.png',
      document: '/pages/offline-2.tsx',
    },
  },
  // ...
})

This is what my offline page looks like (image credit: Craftwork Design): PWA offline page

Troubleshooting tips

  • If you want to test it in the local dev environment, and see an error that looks something like this, you might need to install webpack as a dev dependency.

    Could not find files for / in .next/build-manifest.json

    To do that, run yarn add webpack --dev.

  • Ensure you keep content updated by adding the following PWA files to .gitignore (if you use git), so that these are not checked in and cached, but generated each time you build your app.

    # .gitignore
    
    /public/sw.js
    /public/workbox-*.js
    /public/worker-*.js
    /public/sw.js.map
    /public/workbox-*.js.map
    /public/worker-*.js.map
    /public/fallback-*.js

Appendix

My manifest.json file:

{
  "name": "Bionic Julia",
  "short_name": "Bionic Julia",
  "description": "My thoughts and learnings",
  "start_url": "/",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "display": "standalone"
}

© 2016-2024 Julia Tan · Powered by Next JS.