Are Service Workers Faster Than The Browser Cache?

Move over AppCache, there's a new API for building interactive, feature-rich applications: Service Workers.

Service workers are JavaScript functions that run in the browser separately from a web page. They allow for long-running tasks like push notifications, background sync, and providing cached versions of pages. They're designed to help website creators provide usable versions of web applications in case of limited or no network connectivity.

For example, consider a website like Google Docs. With Google Docs, most of the work occurs on the client. The server only needs to transmit changes to the document, but the document itself is worked on the client. With service workers, a site like Google Docs could work offline more effectively by caching changes to the local browser queue. Then, when connection is restored, those changes will be sent back to Google's servers and persisted.

Today, service workers are supported by 83% of browsers. Browsers that don't support it include Internet Explorer 11, Edge version 16 and earlier, Safari on iOS version 11.2 and earlier, and Opera Mini.

Important Characteristics About Service Workers

Service workers have the following characteristics:

HTTPS is Required

Service workers are only available over HTTPS. This is to prevent service workers from being modified or replaced in-transit. A malicious service worker could hijack connections, intercept requests and responses, perform cross-site scripting attacks, or cause other forms of damage. Using HTTPS ensures the service worker hasn't been tampered with between the server and your browser.

Can't Access the DOM

Service workers run in a separate thread from the web page they originated from. This prevents them from interacting with the Document Object Model (DOM), so they won't be able to read or modify the page's contents. However, service workers can communicate with pages using postMessage.

Temporary and Immutable

Service workers only run when they're needed, and do not persist information. This means you can't maintain a global state between service worker instances. Instead, you can persist and reuse information by using the IndexedDB API.

Promise-based

Service workers make heavy use of Promises. Service workers must be non-blocking, and therefore can't use synchronous APIs such as localStorage.

How Do I Use Service Workers?

There are a number of different applications for service workers including caching, pushing and retrieving data to and from servers, relaying messages, and supporting offline functionality for websites. You can find several examples in the ServiceWorker Cookbook provided by Mozilla.

As an example, let's create a simple website that displays an image. We'll use a service worker to check the local cache for a copy of the image, and if it's not available, download the image from its original website.

1. Register the Service Worker

When creating a new service worker, you first need to tell the browser where it lives. Registration can take place in another service worker, script, or on the page itself. You can assign it a scope based on the URL, but by default its scope is limited to the folder it's located in.

For example, registering the service worker at "/js/worker.js" fires off the worker for any request whose URL starts with "/js/". This example registers the worker on page load.

# page.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/worker.js').then(function(registration) {
// Successful registration
}, function(err) {
// Unsuccessful registration
});
});
}

After successful installation, the service worker waits for previous versions of the worker to finish before starting. This is to ensure only one version of a service worker is running at a time. When the old service worker is gone and the new one starts, this fires the activate event. This is the ideal time to remove functionality from the old worker, perform upgrades, etc.

2. Installing the Service Worker

After registering the service worker, you now need to install it. The install event runs when the service worker is first fetched. This lets you initialize the service worker before it actually runs.

Continuing with our previous example, we'll add an install method to our service worker file that will download the image to the cache:

# worker.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('site-cache')
.then(function(cache) {
return cache.add(image.png);
})
);
});

3. Defining the fetch Event

Now that the worker is installed, you need to define what happens each time a request originates from within your service worker's scope. These requests are handled by the fetch event. In order words, any request triggered by a page or script in the "/subfolder" URL will pass through this method.

# worker.js
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
// Cache hit
return response;
}

// Cache miss: pass request to server
return fetch(event.request);
})
);
});

Benchmarking Service Workers

Because of their versatility, the true performance benefit of service workers depends on their application. For our purposes, we will be testing the caching performance of service workers compared to the in-browser cache.

To perform the test, we created two websites. Both websites display the same content, but one uses service workers to cache the contents of each page. We deployed each website to an f1-micro Google Compute Engine instance running Nginx 1.10.3. We used the default Nginx settings with the exception of enabling TLS 1.2, since service workers require HTTPs.

For the site's content, we created a simple HTML document displaying the following images from NASA's image library:

Image 1
Image 2
Image 3

We used Sitespeed.io version 7.2.0 to collect performance statistics. We used the --preURL parameter to warm up the cache before each test. For each of the following metrics, we display the mean value (in milliseconds) calculated by Sitespeed.io:

Without Service Workers

For the basic web page, we displayed each image on the page using basic HTML <img> tags.

Metric

Test 1

Test 2

Test 3

Average

FirstPaint

128

134

119

127

BackEndTime

46

46

47

46

ServerResponseTime

46

48

49

48

Last Visual Change

2267

2467

2367

2367

With Service Workers

For the service worker-enabled web page, we replace the <img> tags with a JavaScript function that fetched the images and inserted them into the body of the page. We registered the service worker in the <head> section to ensure that any requests were intercepted by the service worker.

Metric

Test 1

Test 2

Test 3

Average

FirstPaint

741

747

731

740

BackEndTime

51

51

52

51

ServerResponseTime

64

52

64

60

Last Visual Change

1900

1867

1833

1867

With service workers enabled, there was a much longer time-to-first-byte (represented by FirstPaint) but an overall reduction in page load time of 500 ms. This is likely due to the service worker intercepting and checking each request before it begins rendering the page. Since all of the heavy lifting occurs in the browser, the differences in backend and server response times are minor. Clearly service workers are fast, even compared to the in-browser cache.

Conclusion

Service workers are extremely versatile technology with a lot of potential for modern websites. Beyond a simple caching mechanism, service workers can help further bridge the gap between desktop and web applications.