Getting Started With Next.js and Service Workers

Ian Thomas
6 min readJul 11, 2017

--

I recently relaunched the website for the charity Build A School In Kenya and decided to build it using Next.js and deploy using Now.

Next.js is a fantastic framework from the team at Zeit — a US based company working on a cloud platform targeting the JavaScript/Node ecosystem.

One of the benefits of Next.js is the focus on universal rendering to maximise perceived performance for web applications. It’s based on React (although alternative libraries, such as Preact, can be swapped in) and offers one of the slickest developer experiences around.

After deploying the site I ran some benchmarks using webpagetest.org and Google’s Lighthouse tool and found that it came up short on the Progressive Web App checklist.

The prospect of combining Next’s emphasis on performance with the benefits of offline support and pre-fetching of static assets was too tempting so I decided to look into adding service worker support to see if the site could be sped up even further.

Why Use a Service Worker?

If you’ve arrived at this post looking for a simple way to get started with service workers in Next.js, chances are you don’t need selling on the concept. However, if you’re new to the area as a whole (as I was) it’s worth explaining some of their potential benefits.

Caching

The headline offering is faster loading of assets and offline support via caching. Service workers can act like a middleware between your browser and external content (like images, fonts, APIs, etc.) by intercepting HTTP requests made by your browser. This can lead to a number of different strategies for returning assets to users such as initially showing low-res images before swapping in the correct version or implementing a stale-while-revalidate pattern for regularly updated content (e.g. a game leaderboard or news feed).

Background Sync

Storing content in a cache has some drawbacks — what if the app provides an offline display for a customer’s orders and the content is out of date? Thankfully, background sync allows service workers to listen for specific sync events triggered by an application so any cached content can be updated ready for consumption either immediately or when the device is next online.

Background sync also allows us to handle poor network conditions when a user is trying to submit some data back to a service. Instead of forcing users to wait while a slow request is sent or having to inform them that a request failed because the network connection was down it’s possible to use a sync to complete the request in the background when connectivity comes back.

Push Notifications

Push notifications have had a huge impact for native application engagement rates but traditionally have been out of reach for web applications. The Push API(1) enables native style push notifications to be received from a server, whether or not the web app is in the foreground (or even loaded).

There’s a lot of literature around on the web to support the use of asynchronous messaging to raise engagement rates with app users so this will be a big step forward for web apps once browser support is better. Speaking of which…

Browser Support

Service worker support is gradually improving, with full support in Chrome, Firefox and Opera and partial support in MS Edge. Webkit has signalled that they are under consideration with meeting notes from 2015 saying:

Service Workers — becoming a more frequent request. We should do it

Working With Next.js

The filesystem plays a huge part in Next’s routing, with the pages folder providing your application's routes (a route is created per .js file). These files are automatically transpiled, server rendered and updated via hot code reloading (in a non-production environment) and they are central to the Next.js framework.

There’s also a special static folder which is handled slightly differently. Any content in this folder will be available directly at your.url.com/static/<<filename>> and can be of any type.

It’s worth noting at this point that any .js files in the static folder won't be transpiled by Next.js automatically. It is possible to set up custom config for Webpack and Babel to cover them if you need to, alternatively create a separate config for static assets that require transpiling.

Service Worker Scope

This post isn’t an exhaustive look at how service workers are implemented or what they are used for. If you’re interested in finding out more about their background there’s a host of great resources on MDN and the Google Developer Portal.

By default a Service Worker is scoped to ./ from the path on which it is loaded. For example, in our Next.js application a service worker loaded from app.com/static/sw.js would be scoped to the /static path.

Practically speaking this limits the effectiveness of the service worker to intercepting requests to any path below /static. This is absolutely fine for a lot of use-cases, however, if we're aiming for full compatibility with the Lighthouse PWA checklist we will fail to cache any of our pages for offline access.

Ideally, the service worker to be registered would be loaded from the root scope (i.e. https://my.app.com/sw.js) which, in Next.js apps, requires a custom server implementation.

Creating a Basic Service Worker

For the purposes of this article, the service worker implementation is going to be extremely simple (purely caching the index page of the application). The following code was created in a the file ./offline/serviceWorker.js:

const CACHE_NAME = "simple-cache-v1";
const urlsToCache = ["/"];

self.addEventListener("install", event => {
const preLoaded = caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
event.waitUntil(preLoaded);
});

self.addEventListener("fetch", event => {
const response = caches.match(event.request)
.then(match => match || fetch(event.request));
event.respondWith(response);
});

All this code does is listen for the install event and when triggered it pre-fetches all the urls specified in urlsToCache. Then it adds a fetch listener to intercept requests to content under the scope of the service worker and, if present, returns the pre-fetched response from the cache.

Important to note is how much of the service worker API relies on promises so it’s worth revising them if you haven’t worked with them much.

Then, in the ./pages/index.js component the service worker needs to be registered (if supported).

class Index extends React.Component {
componentDidMount = () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js")
.catch(err => console.error("Service worker registration failed", err);
} else {
console.log("Service worker not supported");
}
}
render() {
return <div>Hello World</div>
}
}

Setting Up a Custom Server

The out of the box developer experience when working with Next.js is very slick, with hot code reloading, automatic server restarts and transpilation using Babel built in by default.

Unfortunately the default setup doesn’t allow us an easy way to scope the service worker to our root path so a custom server implementation is needed.

The following ./server.js is enough to make everything work as expected:

const { createServer } = require('http');
const { parse } = require('url');
const { createReadStream } = require('fs');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
const { pathname } = parsedUrl;

if (pathname === '/sw.js') {
res.setHeader('content-type', 'text/javascript');
createReadStream('./offline/serviceWorker.js').pipe(res);
} else {
handle(req, res, parsedUrl);
}
}).listen(3000, err => {
if (err) throw err;
console.log('> Ready on http://localhost:3000');
});
});

Note that the handling of ports for the server doesn’t matter when deploying using Now. This would need to be configured via the environment for other deployment targets (e.g. Heroku)

Deploying With Now

Once the app is ready, testing it out is as simple as running next in your terminal. However, Zeit also offers a powerful and flexible deployment solution called Now.

Getting setup with Now is easy (see their guides) and deploying a Next.js application is even simpler. Run now in the root of your project and the application will be automatically deployed to a new url on the now.sh domain.

Going Beyond The Basics

This article only scratches on the surface of what is possible with both Next.js and the service worker API. Some interesting next steps would be to implement background sync for updating pages in the browser cache or to start implementing push notifications via your new service worker.

Using these technologies offers many different ways to improve the UX of web applications and with browser support increasing regularly now is a good time to start investigating them properly.

For more information about Next.js or Now, head over to the Zeit website or join their Slack chat. While not a viable option for all types of app they are an interesting alternative to traditional JavaScript frameworks and deployment solutions.

  1. Apple being Apple have decided to do away with the standards based approach and have instead implemented push notifications using their proprietary Apple Push Notifications Service (Link) ↩︎

--

--

Ian Thomas

Hi there 👋 I’m VP Web Architecture for Genesis Global and I'm interested in Software Architecture, Socio-Technical Systems and Staff+ Engineering Roles