How to Add Service Worker to Your Next.js App
Service workers enhance your website to enable offline experience in your progressive web application PWA, manage network calls with cache, handle push notifications, and defer tasks to run once the device is back online. It's a powerful tool to improve user experience. In this blog we will be learning how to add service workers to your Next.js App.

Jordan Wu
10 min·Posted

Table of Contents
What are Service Workers?
Service worker is an event-driven web Worker that handles network requests. It sits between your web application, the browser, and the network. It handles intercepting and modifying navigation and resource requests, and caching resources in a very granular fashion to give you complete control over how your app works. It runs on a different thread to the main JavaScript that powers your app, so it is non-blocking by design to be fully async and only runs over HTTPS.
The Service Worker Lifecycle
The scope of your service worker is determined by where you place the worker relative to your web app root level. Only one service worker per scope is allowed and this applies to browser tabs and PWA windows. It's recommended to place the service worker at the root level as that will allow it to intercept all the requests related to your PWA. The lifecycle is determine by the following states:
Download
The service worker lifecycle starts with registering the service worker. The browser then attempts to download and parse the service worker file.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
Install
If parsing succeeds, its install
event is fired. The install
event only fires once. Service worker installation happens silently, without requiring user permission, even if the user doesn't install the PWA. The point where this event fires is generally a good time to precache your website assets using the built in Cache.
self.addEventListener('install', (event) => {
console.log('Service worker installed')
})
Activate
After the installation, the service worker is not yet in control of its clients, including your PWA. It needs to be activated first. When the service worker is ready to control its clients, the activate
event will fire. By default, the service worker will not take control until the next time you navigate to that page, either due to reloading the page or re-opening the PWA. The point where this event fires is generally a good time to clean up old caches and other things associated with the previous version of your service worker.
self.addEventListener('activate', (event) => {
console.log('Service worker activated')
})
Waiting
Service workers get updated when the browser detects that the service worker currently controlling the client and the new (from your server) version of the same file are byte-different. After a successful installation, the new service worker will wait to activate until the existing (old) service worker no longer controls any clients. This state is called "waiting", and it's how the browser ensures that only one version of your service worker is running at a time. Refreshing a page or reopening the PWA won't make the new service worker take control. The user needs to close or navigate away from all tabs and windows using the current service worker and then navigate back. Only then will the new service worker take control.
Service worker lifespan
Once installed and registered, a service worker can manage all network requests within its scope. It runs on its own thread, with activation and termination controlled by the browser. Service workers don't live indefinitely. While exact timings differ between browsers, service workers will be terminated if they've been idle for a few seconds, or if they've been busy for too long. If a service worker has been terminated and an event occurs that would start it up, it will restart.
The fetch
event let us intercept every network request made by the PWA in the service worker's scope, for both same-origin and cross-origin requests. The fetch
handler receives all requests from an app, including URLs and HTTP headers, and lets the app developer decide how to process them. For example, your service worker can forward a request to the network, respond with a previously cached response, or create a new response. The choice is yours.
self.addEventListener('fetch', (event) => {
console.log(`URL requested: ${event.request.url}`)
})
Caching Strategies
Caching strategies determine how to handle network requests with the cache for common use cases:
Cache First
Using this strategy, the service worker looks for the matching request in the cache and returns the corresponding Response if it's cached. Otherwise it retrieves the response from the network (optionally, updating the cache for future calls). If there is neither a cache response nor a network response, the request will error. Since serving assets without going to the network tends to be faster, this strategy prioritizes performance over freshness.
Network First
This strategy is the mirror of the Cache First strategy; it checks if the request can be fulfilled from the network and, if it can't, tries to retrieve it from the cache. Like cache first. If there is neither a network response nor a cache response, the request will error. Getting the response from the network is usually slower than getting it from the cache, this strategy prioritizes updated content instead of performance.
Stale While Revalidate
The stale while revalidate strategy returns a cached response immediately, then checks the network for an update, replacing the cached response if one is found. This strategy always makes a network request, because even if a cached resource is found, it will try to update what was in the cache with what was received from the network, to use the updated version in the next request. This strategy, therefore, provides a way for you to benefit from the quick serving of the cache first strategy and update the cache in the background.
Network-Only
The network only strategy is similar to how browsers behave without a service worker or the Cache Storage API. Requests will only return a resource if it can be fetched from the network. This is often useful for resources like online-only API requests.
Cache-Only
The cache only strategy ensures that requests never go to the network; all incoming requests are responded to with a pre-populated cache item. The following code uses the fetch event handler with the match method of the cache storage to respond cache only:
Precaching And Runtime Caching
The interaction between a service worker and a Cache
instance involves two distinct caching concepts: precaching and runtime caching. Each of these is central to the benefits a service worker can provide.
Precaching is the process of caching assets ahead of time, typically during a service worker's installation. With precaching, key static assets and materials needed for offline access can be downloaded and stored in a Cache instance. This kind of caching also improves page speed to subsequent pages that require the precached assets. Precache critical static assets like global stylesheets, global Javascript files, Application shell HTML, and offline fallback. Only precache what you need and don't precache responsive images or favicons along with polyfills.
Runtime caching is when a caching strategy is applied to assets as they are requested from the network during runtime. This kind of caching is useful because it guarantees offline access to pages and assets the user has already visited.
How to Deploy a No-op Service Worker
Sometimes a buggy service worker gets deployed, and then there are problems. It could cause your web app not to work as expected. If this is the case you would want to deploy a basic no-op service worker that installs and activates immediately without a fetch event handler:
self.addEventListener('install', () => {
// Skip over the "waiting" lifecycle state, to ensure that our
// new service worker is activated immediately, even if there's
// another tab open controlled by our older service worker code.
self.skipWaiting()
})
self.addEventListener('activate', () => {
// Optional: Get a list of all the current open windows/tabs under
// our service worker's control, and force them to reload.
// This can "unbreak" any open windows/tabs as soon as the new
// service worker activates, rather than users having to manually reload.
self.clients
.matchAll({
type: 'window',
})
.then((windowClients) => {
windowClients.forEach((windowClient) => {
windowClient.navigate(windowClient.url)
})
})
})
Add Service Worker to Next.js App
To add service workers to your Next.js App you will be using a library Workbox make maintaining your service worker and cache storage logic easier and avoid common mistakes when using service workers. It encapsulates both the Service Worker API and Cache Storage API, and exposes more developer-friendly interfaces.
The workbox-window
module is useful to simplify service worker registration and
to prevent developers from making common mistakes, such as registering a service worker in the wrong
scope.
pnpm add workbox-window
I created a ServiceWorker.ts
file to call the registration logic once your app mounts.
'use client'
import { useEffect } from 'react'
import { Workbox } from 'workbox-window'
export default function ServiceWorker() {
useEffect(() => {
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js')
wb.register()
}
}, [])
return null
}
As mentioned earlier it's recommended to place the service worker at the root level as that will allow it to intercept all the requests related to your PWA. This means you would want to place your logic inside of root layout.ts
.
import ServiceWorker from '@/components/ServiceWorker'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<main>{children}</main>
<ServiceWorker />
</body>
</html>
)
}
The workbox-sw
module provides an extremely easy way to get up and running with the Workbox
modules, simplifies the loading of the Workbox modules, and offers some simple helper methods. The easiest
a way to start using this module is to install it via the content delivery network CDN. Once imported you
will have access to all the packages.
The workbox-recipes
module is used to add common patterns around routing and caching that have been standardized into reusable recipes. To learn more check out workbox-recipes.
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.1.0/workbox-sw.js')
const { pageCache, imageCache, staticResourceCache, googleFontsCache, offlineFallback } = workbox.recipes
pageCache()
// Might not need google font cache if you load your fonts using 'next/font/google'
googleFontsCache()
staticResourceCache()
imageCache()
// There's addition steps to get the offline fallback recipe working with Next.js
offlineFallback()
Summary
Adding service workers to your PWA will enhance your PWA the ability to still work offline. An example would be an event ticket app. The users would still want to be able to show their tickets with or without the internet. Be sure to use Service worker tools and Storage tools to help debug.