DEV Community

Cover image for Implementing service workers in your Next.js app
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Implementing service workers in your Next.js app

Written by Elijah Agbonze✏️

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:

What are service workers?

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.

Use cases for service workers in Next.js

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

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.

PWAs

Progressive web apps require service workers for custom network requests, offline support, fast loading, push notifications, and many other functionalities.

Push notifications

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.

Background sync

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.

Analytics

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.

How service workers work

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:

  1. Register
  2. Install
  3. Activate

Let’s walk through each of these steps now.

Registering a service worker

Below is an example of registering a service worker:

window.addEventListener('load', () => {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js');
  };
})
Enter fullscreen mode Exit fullscreen mode

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.

Installing and activating a service worker

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')
});
Enter fullscreen mode Exit fullscreen mode

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.

Implementing service workers in Next.js

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.

Setting up a Next.js project

Firstly, let’s create a Next.js project. To do that, run the command below:

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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: Browser Devtools Open To Application Tab With Service Workers Folder Open

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.

Adding a service worker

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:

  • Register a service worker in a global scope environment
  • Create a service worker file which will contain all of our service worker code

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));
    }
  }, []);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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: Service Workers Tab In Browser Devtools Displaying Application Information With Offline Box Unchecked And Update On Reload Box Checked You should see something like the image above on yours. The status is currently indicating that the service worker is activated and running.

Caching Next.js assets using service workers

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();
Enter fullscreen mode Exit fullscreen mode

You should have something like this in your cache: Application Cache Open In Browser Devtools

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));
    }
  }, []);
Enter fullscreen mode Exit fullscreen mode

Using the Push API in service workers

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.

Subscribe for push notifications

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,
   })
 );
Enter fullscreen mode Exit fullscreen mode

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.

Listen for messages

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);
   }
 });
});
Enter fullscreen mode Exit fullscreen mode

Drawbacks to using service workers in Next.js

There are some common concerns you should be aware of before diving into using service workers in your Next.js application.

Cache invalidation

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.

Limited browser support

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.

Reduced performance

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.

Security risks

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.

Conclusion

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.


LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next 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.

Top comments (0)