BLOG

Making an offline first app | Caching for the modern web


While working on this portfolio, I implemented my first service worker hoping to cache the images used and provide faster renders to the returning user. In effect, I realised that there was so much more to be achieved. But let's start with the basics here.

What is an offline first app?

An offline first app is an app which is able to fulfill its purpose even when offline or in a flaky internet connection. This is important because many mobile users are on the move while they browse, maybe in a subway or travelling in a cab, and they will potentially lose internet connectivity every few mins.

Try turning off your internet connection and refreshing this webpage.

Option+Cmd+I on mac to inspect> Application> Service Workers> Check the offline checkbox to mark the current tab offline.

The webpage still works!! Grande!!

The offline experience for a web app can range from showcasing a simple offline page, to a readonly view of a limited section of data, to a semi/fully functional flow that syncs when the network is available. All of this is possible by the abilities of service workers.

So, what's a service worker?

A service worker is a script that runs in the background in the browser, which has access to browser features like push notifications, network interception etc. It opens up a variety of features that do not need a user interface.

Things to note about service workers -

  • Service Workers are non blocking, async in nature.
  • For security reasons, service workers can only run over HTTPS
  • Service workers are a programmable network proxy, allowing the developer to control all network requests for the scope in the domain.
  • Being a Javascript Worker, Service Workers do not have a direct access to the DOM. If required, the postmessage interface can be used to communicate with the main thread.
  • The lifecycle of a service worker is entirely different from that of the web app that it governs. Here's a good reliable thread for the lifecycle of a service worker.

A service worker that does nothing

A Hello Service worker program might be the toughest Hello world code you might ever have to write. That being said, it's not that tough.

The first step write your own service worker is to register it.

if ('serviceWorker' in navigator){ window.addEventListener('load', ()=>{ navigator.serviceWorker.register('/* path to service worker */') .then((sw) =>{ console.log('Hello Service Worker',sw); }); });};

When run in the main JS thread, this should successfully register the empty service worker. Kudos when you see the message light up in your console.

Caching a lot of stuff!

// A filter to define the types of requests to be cachedconst requestsToBeCached = ['xyz.com', 'abc.com'];
self.addEventListener('fetch', event => { const normalizedUrl = new URL (event.request.url); // Normalizing because query params do not impact the file sent normalizedUrl.search = "";
if(requestsToBeCached.includes(normalizedUrl.origin)){ event.respondWith( (async function() { const fetchResponseP = fetch (event.request.url); // Creating a clone as the request will be consumed in cache const fetchResponseCloneP = fetchResponseP.then(r =>r.clone());
event.waitUntil( (async function() { const cache = await caches.open(/*CACHE NAME*/); await cache.put(normalizedUrl, await fetchResponseCloneP); })() }; return (await caches.match(normalizedUrl)) || fetchResponseP; })() }; }});

The above code in a service worker would cache all requests from xyz.com and abc.com

So, what just happened?

Though the code above is pretty self explanatory, here's the gist of it all -

  • The first step to caching is to intercept all network requests in the service worker.
  • We've chosen to restrict the requests that are being cached. This might not be needed for all websites. Some static websites might choose to simply cache everything so the filtering logic is not needed in such cases, while another good usage would be to just cache the assets consumed on a site example.com/assets.
  • After capturing our desired set of requests, we check whether the requested resource is already present is the cache. If the resource is present, we return it, otherwise, we wait for the network response and return that. ( Line 16 )
  • When the web resource is loaded for the first time, we'd have to save it in the cache. We create a copy of it because putting it in the cache consumes the request, thus duplicate of the request is used to cache. ( Line 8 - 14 )
  • event.waitUntil is used to keep the event alive for caching.

Offline first is a fairly straight forward concept on paper, yet the implementations can vary largely based on the priorities and the level of optimizations needed. There are multiple caching strategies to consider, what we have done above is just one way to do it.

Some simpler ways to use caching

Cache Only

self.addEventListener('fetch', event => { event.respondWith(caches.match(normalizedUrl));};
  • Will lead to network errors if the requested resource is not present in cache
  • Barely ever recommended. Maybe try for static resources.

Cache, fallback to network

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

Network, fallback to cache

self.addEventListener('fetch', event => { event.respondWith(fetch(event.request) .catch(err =>{ return caches.match(event.request) }) );});

There can be many more caching strategies, each more complex then another, serving its usecase.


Hi! I'm a web developer making assets on the internet. Think you have something to say about the article? Do reach out to me at saranshgupta1995@gmail.com