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 —
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.
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.
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 😊