Over the years, developers have tried many different strategies for making web apps as close to native apps as possible in terms of user experience. One issue that has been a significant challenge to solve is what happens when network connectivity has been lost.
As a native app user, you’d expect to be still able to access the app’s data. But the case is different in web apps.
There have been many solutions to try and solve this, but the endpoint problem has always been caching assets and making custom network requests. Service workers fix this issue.
With service workers, you can provide an offline-first experience by setting up your app to use cached assets. This way, when a user is offline, they would still be able to access previous data.
Service workers have been widely adopted by the dev community, mainly because of how useful they have proven to be in progressive web apps (PWA). However, there are many other use cases that make service workers very useful in our everyday web apps.
In this article, we will look at how we can implement service workers in Next.js. We will cover:
Service workers are scripts that run in the background of a web page. They are completely separate from the main browser thread, and as such, they are non-blocking and have no access to the DOM whatsoever.
Since service workers act as a proxy between the web page and the network, they can provide features such as offline support, push notifications, and background updates. They intercept network requests made by the web page and modify or respond to them directly without involving the page itself.
Service workers are designed to be fully async. As such, APIs such as XHR and web storage — including both localStorage
and sessionStorage
— can’t be used in service workers.
It is important to note that service workers only run on HTTPS with the exception of a localhost
server only. Running over HTTPS helps prevent malicious security threats that can compromise your app and its users. So, only use HTTP in development.
Before we dive deep into implementing service workers in your Next.js app, let’s take a look at some of the features it can be used for.
Caching your assets allows you to provide an offline-first experience for your users.
To achieve this, you’d have to use the Cache API to store your assets in the browser cache, also known as precaching. When connection is unavailable, users can get the assets from the cache, also known as runtime caching.
Progressive web apps require service workers for custom network requests, offline support, fast loading, push notifications, and many other functionalities.
With service workers, you can listen for push notification events, which will enable your app to receive notifications even when the app is not actively running in the browser. Push notifications are super useful for apps that need to deliver real-time updates, such as social networks.
Service workers can enable background synchronization of data between the page and the server, even when the app is not actively running. Background sync makes it possible for the user’s data as well as your server to always be up to date, which will reduce the risk of data loss.
Service workers can be used to collect analytics data without interrupting the user’s experience. Analytics can be very helpful for providing insights into app performance.
Before we jump into implementing service workers in Nextjs, let’s take a look at how they work traditionally. There are three main steps carried out when using service workers:
Let’s walk through each of these steps now.
Below is an example of registering a service worker:
window.addEventListener('load', () => { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js'); }; })
Here, we checked to see if the ServiceWorker API is supported by the browser and activated before proceeding to register one. The first param of the register
function is the path to the service worker file.
Now in the service worker file, you’ll need to install a service worker and activate it using the install
and activate
events, respectively:
self.addEventListener('install', () => { console.log('service worker installed') }); self.addEventListener('activate', () => { console.log('service worker activated') });
self
is similar to window
in the context of a regular web page. It is used instead of window
to avoid confusion with it, as the global window
object is not available in a service worker.
Right now we’re only logging messages to the console to indicate when these events take place. However, there’s a lot more that can be done inside of these functions.
For example, we could create a cache and add assets to it when the install
event is complete. We will see examples of various implementations in the next section.
In this section, we will build a basic Next.js application and make it available offline using service workers. To follow along with this tutorial, you should be familiar with using Next.js for development.
Check out the live demo of this project. You can also access the code through GitHub.
Firstly, let’s create a Next.js project. To do that, run the command below:
npx create-next-app@latest
Once installed, open the project in your preferred code editor and run npm run dev
in the terminal window.
For the sake of this tutorial, we will create an additional page called docs
. So head up to the pages
directory, create a new file called docs.js
, and paste the following in it:
import Head from 'next/head'; import Image from 'next/image'; import styles from '@/styles/Home.module.css'; export default function Docs() { return ( <> <Head> <title>Next.js Service Workers Docs</title> <meta name='description' content='Generated by create next app' /> <meta name='viewport' content='width=device-width, initial-scale=1' /> <link rel='icon' href='/favicon.ico' /> </Head> <main className={styles.main}> <div className={styles.description}> <p>Service Worker Docs</p> <div> <a href='https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app' target='_blank' rel='noopener noreferrer' > By not{' '} <Image src='/vercel.svg' alt='Vercel Logo' className={styles.vercelLogo} width={100} height={24} priority /> </a> </div> </div> <div className={styles.center}> <Image className={styles.logo} src='/next.svg' alt='Next.js Logo' width={180} height={37} priority /> </div> </main> </> ); }
This page is similar to what we have in index.js
. Now when you open the project up in your browser, open up the browser’s DevTools and navigate to your application. Select the “Service Workers” tab:
This is where you’ll see all of your service workers in action — i.e., in the process of being installed, activated, and used.
Check the “Offline” box and reload your page. Obviously, your browser would then indicate that there is no internet.
Then, take a look at the demo website for our project and check the “Offline” option as well. You should notice the page still loads fine. By the end of this tutorial, yours will be able to do that as well.
Using service workers in Next.js before v10 would require a local server. However, starting from Next.js v10, service workers can be used without a local server. It’s similar to implementing it in vanilla JS.
For this to work, there are two things we need to do:
First, let’s register a service worker in _app.js
by adding the following in the function:
useEffect(() => { if ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service-worker.js') .then((registration) => console.log('scope is: ', registration.scope)); } }, []);
The reason I’m registering this service worker in _app.js
is because I want it to have access to the full website — all assets, pages, everything. In large-scale applications, you may only need a particular service worker for a certain group of pages, and another service worker for a different group of pages.
As mentioned earlier, the first parameter of the register
method is the path to the service worker. The browser should be able to access this path. In Next.js, for the path to be accessed by the browser, it would be in the public
directory.
Head up to the public
directory and create a new file called service-worker.js
. This is the file in which we will install and activate our service worker, as well as do other interesting things like caching, push notifications, and more.
Before we jump into caching, let’s first install and activate the service worker. Paste the following in the service-worker.js
file:
const installEvent = () => { self.addEventListener('install', () => { console.log('service worker installed'); }); }; installEvent(); const activateEvent = () => { self.addEventListener('activate', () => { console.log('service worker activated'); }); }; activateEvent();
You should recognize this code from our overview of installing and activating a service worker above.
Save the file and reload the page in the browser, making sure to uncheck “Offline” in the DevTools panel. You should also check the “Update on reload” option so as to update the service worker when you reload the page:
You should see something like the image above on yours. The status is currently indicating that the service worker is activated and running.
Let’s add offline support for our page. Traditionally, what we would do is to list all of our assets and pages and cache each one of them.
However, that’s a lot of work for a Next.js project — you’d have to find the unique ID of each CSS file, as well as list each JavaScript static file that represents your pages after the build of your application.
An easier approach would be to cache all the assets of the current page the user is on at once. We can achieve this by using the clone()
method of the Response
object in the fetch
event handler of the service worker.
Add the following to your service-worker.js
file:
const cacheName = 'v1' const cacheClone = async (e) => { const res = await fetch(e.request); const resClone = res.clone(); const cache = await caches.open(cacheName); await cache.put(e.request, resClone); return res; }; const fetchEvent = () => { self.addEventListener('fetch', (e) => { e.respondWith( cacheClone(e) .catch(() => caches.match(e.request)) .then((res) => res) ); }); }; fetchEvent();
You should have something like this in your cache:
Now check the “Offline” option and reload the page.
Notice the /docs
page is not yet included in the cache. This is because it hasn’t been visited. After you navigate to the page, it will be added to the cache along with any other new assets pertaining to that page.
This may not be a very good practice for a large-scale application with lots of assets. As such, you may want to consider caching only to the limited scope of the service worker.
For example, if the service worker scope is limited to only the /docs
page, the /index
page and its assets will not be cached. You can specify scopes for a service worker with the scope
property while registering the service worker like so:
useEffect(() => { if ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service-worker.js', { scope: '/docs' }) .then((registration) => console.log('scope is: ', registration.scope)); } }, []);
The Push API provides your web app the ability to receive push notifications from your server and display them to your user.
Remember, service workers run in the background of your app. As such, the app doesn’t need to be running in the browser for the push notification to be received by the browser and displayed to the user.
Let’s see a basic example of how we can implement the Push API using Next.js service workers.
Firstly, you’d have to register your app for push notifications with the browser:
navigator.serviceWorker .register("/service-worker.js") .then((registration) => registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey, }) );
The userVisibleOnly
option indicates that messages from the push subscription will only be visible to the user.
The applicationServerKey
option is used to specify a public key that will be used by the push service provider to authenticate your server.
This key can be a JWT sent from your server to be used in the subscription process.
The final thing to do is listen for messages from your server and display them to the user. Registering your web app for push notifications allows you to use the PushEvent
, which listens for notifications from your authenticated server:
self.addEventListener('push',(event) => { const data = event.data.json(); const title = data.title; const body = data.message; const icon = 'some-icon.png'; const notificationOptions = { body: body, tag: 'simple-push-notification-example', icon: icon }; return self.Notification.requestPermission().then((permission) => { if (permission === 'granted') { return new self.Notification(title, notificationOptions); } }); });
There are some common concerns you should be aware of before diving into using service workers in your Next.js application.
Sometimes it can be challenging to ensure that the cached assets are always up to date. This can result in users using outdated resources on your website.
To solve this, versioning your cache — as we’ve seen in this article — is highly recommended. For each new asset or resource, create a new cache with a different version.
Although modern popular browsers support service workers, it is also worth noting that there are some older browsers without support for service workers.
If you’re sure most of your users use old browsers, then you might want to create your website in such a way that it doesn’t rely too much on service workers.
Traditionally, service workers only provide additional enhancements for existing websites. In other words, they bring improved functionalities like offline availability, background syncing, push notifications, analytics, etc to your website.
As a result, the browser not supporting service workers will not break your application; they work like outside-the-box plugins that your existing website does not depend on to function.
Service workers are there to improve the performance of your web app. However, if poorly implemented, they could have a negative impact on your app’s performance.
For example, caching too many assets can cause slower load times. To prevent this issue, consider scoping your service workers, as mentioned earlier.
Using service workers on an unsecured protocol can be a security threat to both you and your users. This is because service workers can be used to intercept network requests and responses, which can be used to steal sensitive information from your users.
Service workers can also be exploited to perform malicious attacks such as phishing and spamming, which can pose a threat to your app’s reputation. So, you shouldn’t risk using service workers over HTTP in production.
In this article, we looked at what service workers are, some common use cases for them, and how we can implement them in Next.js.
Let me know what you think about using service workers in the comments below. Thanks for reading and happy hacking.
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
One Reply to "Implementing service workers in your Next.js app"
Thank you for sharing this, na only God go bless u