Custom Service Worker in CRA(Create React App)
Hi there! This is my first post so far on Medium. It covers customizing and extending the built-in service worker in a React application curated using CRA boilerplate.
What is a service worker?
A service worker is a script that runs on a background thread and acts as a network proxy for the web application. It has capabilities such as intercepting all network requests within specified scope, caching files, background sync, push notification, etc which makes it very useful for adding offline support in a site or building a PWA(Progressive Web App).
Please check this link for more reference from Google web fundamentals.
Things to Note:
- A service worker is a script that runs on an independent thread of the browser(not blocking the main thread). Hence, it can run in the background even when there is no webpage.
- It can not access the DOM directly. It is referenced with an origin.
- The web application must be running on HTTPS or localhost.
- The service worker can access indexedDB, cache.
- It can only intercept requests from clients made under its scope. The max scope of a service worker is the location of the worker in the server root. For example,
Case-A
/root
/service-worker.js
...Case-B
/root
/home
/service-worker.js
...
/about
...
In Case-A, the service worker can intercept every request made while in Case-B, it can intercept only the requests made from scripts under /home
.
NOTE: In this post, I’m going to use Workbox to create a service worker. Workbox is an SDK that takes care of most of the boilerplate code while generating a service worker configuration.
The problem
Thecreate-react-app
does provide a service worker out-of-the-box but only with limited capabilities i.e. precaching the assets under /build/static
. You can find a number of pull-requests still under open/review state in the public CRA repository in order to enhance the in-built service worker.
This post will guide a workaround for extending the capabilities of the in-built SW and gaining more control.
Now, without further ado, let’s get started!
First things first!
- Let the CRA implementation take care of the service worker registration and updates(mostly boilerplate and heavy lifting). We’re gonna take control of the installation and add the business logic.
- We’re using Workbox as the service worker build tool. Hence, create the following files with no content and to be filled under /src as,
- src/sw-build.js → For providing instructions to generate the customized service worker
- src/sw-custom.js → To add our custom rules
Creating Build SW script
Since our files are generated dynamically using react-scripts
, we need a way to know the file names to precache them. The Workbox has a builtin method injectManifest that takes care of this. You only have to mention the file patterns that need to be scanned and the destination where they should be mentioned to precache. You can read more about it here.
Add the following code snippet in src/sw-build.js ,
swSrc
: Path to our custom service worker rules
swDest
: Destination path where the final service worker file is created
globDirectory
: Directory to look for files that need preaching
globPatterns
: Match files matching this regex
maximumFileSizeToCacheInBytes
: As the name suggests, we’ll cache all files under 5MB sizes
In the above script, we’ve specified the workbox to look for file patterns specified under globPatterns
and write them under swSrc
.
Now this function looks for the following placeholder under swSrc
to write the generated file names,
workbox.precaching.precacheAndRoute([]);
Adding custom rules to our service worker
- In order to work with the workbox service worker libraries, let’s initialize the global workbox.
- Please refer to this documentation for workbox-sw library methods usage. The code snippet below includes sample caching rules for different file types.
Now update the content of your src/sw-custom.js with this.
Explanation
- importScripts is a part of the WorkerGlobalScope Web API that loads the requested javascript file, in our case the workbox.
- Once the global workbox is ready for use, the custom configurations can be added using workbox.setConfig such as toggle debug logs depending on the environment.
- self is a read-only variable and part of the WorkBoxGlobalScope Web API that may refer to either the global or any specific scope(DedicatedWorkerGlobalScope, ServiceWorkerGlobalScope, etc)
- The rest of the code is very much implicitly understood by the comments. If not, I’d love to discuss them in the comments :)
Replace existing SW
I’m using create-react-app@3.2.0
to create the React application with typescript support.
- Once, the application is created, open
src/serviceWorker.ts
. - You will see a named export method
register
. It contains a window load event-listener from where the registration steps boot up. The default service worker URL is specified there. Let’s replace it with our own!
export function register(config?: Config) {
if(
process.env.NODE_ENV === "production" &&
"serviceWorker" in navigator
) {
...
window.addEventListener("load", () => { // BEFORE
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; // AFTER
const swUrl = `${process.env.PUBLIC_URL}/sw.js`;
...
4. Now change the serviceWorker.unregister()
to serviceWorker.register()
under the src/index.tsx
.
That’s it! Now the service worker is going to be loaded from our custom scriptbuild/sw.js
.
For Typescript Only
Since we’ve generated the SW script in JS, you might wanna exclude it from your TS compiler checks(to avoid unnecessary warnings and errors). Below is a sample configuration in the tsconfig.json
to exclude the SW scripts,
{...
"include": ["src"],
"exclude": ["src/sw*", ...]}
Build Steps
I’m presuming the builds are running in a Node/Linux environment. We’re going to add 2 more available scripts:
build-sw
— To build the custom service workerclean-cra-sw
— To clean the existing CRA service worker
Let’s run these commands post build
. Now, the updatedpackage.json
should look like,
"scripts": {
...
"build-sw": "node ./src/sw-build.js", "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js", "build": "react-scripts build && npm run build-sw && npm run clean-cra-sw", ...}
Handling Updates [important]
The service worker is considered new even if a single byte in the sw.js
file is changed. Hence it undergoes the service worker life-cycle methods once again. That means the new service worker must be installed(by preaching the files) and then activated(after the cache is updated with new files).
Now ideally the service worker waits to be activated after all the opened tabs for our site are closed. However, we’ve usedself.skipWaiting()
in our custom rules which forces our worker to skip to activated
state. So only a reload of the current tab should suffice for the new changes to be available.
Hence the user must be prompted to reload the page when an update is available to always experience the latest changes. The update event is inherently triggered by our boilerplate service worker code. So you just need to supply an object containing the update handler as an argument to the service worker register method.
For example, under your src/index.tsx file update the register call with
const updateHandler = (registration: ServiceWorkerRegistration) => {
if (window.confirm("Update available. Do you want to reload?")){
window.location.reload();
}
};serviceWorker.register({
onUpdate: updateHandler,
});
That’s all. Now, build and run your application and open your browser to check the service worker in action. In chrome, you can verify this by checking the Application tab e.g.,
At LoginRadius, using the service worker saved us almost 60% load time on subsequent loads which helped us achieve user satisfaction more than 80% from the previous 55%.
P.S this post was inspired by the Workbox usage GitHub issue. Also, do check out this youtube link for PWA in action.
Thanks for reading it and do let me know your views in the comments :)