Offline web apps in the wild

There are quite a few offline web apps around, and their number is growing everyday. If you are a developer, you probably know lodash, a very popular JavaScript utility library. I was surprised when I discovered that the library docs work offline! You still have to visit the webpage while being online at least once, so that the ServiceWorker can do its initial job and put the “offline spells”.

Mobile Twitter is another example, although their web application is in constant development (as they said here), and all you can see now while being offline is just a clean UI, without access to any tweets or messages. Still, I find it nicer than the browser’s default “offline” screen.

The last one to mention is a tic-tac-toe game I wrote for the purpose of an article about react, redux and create-react-app. I encourage you to follow the link, then go offline and reload the page. It still works!

Psst, you don’t have to disconnect from your wi-fi or unplug the cable. If you are using Chrome, simply open developer tools, switch to the “Network” tab and click the “offline” checkbox at the top:

Chrome’s dev tools let you simulate offline activity without disabling your real connection.

After you reload the webpage, take a look at the list of static assets (HTML, CSS, scripts). The “size” column clearly says that the content was served from the ServiceWorker:

Thank you, ServiceWorker!

Browser workers

Before we look at ServiceWorker in details, let’s talk about the “generic” workers first. There are two, widely-known, standard types of workers: Worker (sometimes referred to as WebWorker; think of it as a separate thread of execution for tasks that can be offloaded from the main thread) and ServiceWorker (there are actually more, like SharedWorker or non-standard ChromeWorker , but that’s a topic for a whole another story).

All workers share common features:

they execute in their own, separate threads (yes, it is separate from the main thread),

they have no access to DOM or other synchronous APIs (like LocalStorage ) — mostly to prevent data races,

) — mostly to prevent data races, they do, however, have access to IndexedDB, a transactional database embedded in the browser,

they use postMessage API for both-ways asynchronous communication with the main thread.

There’s a key difference, however: many Workers can “live” on a website at the same time, but at most one ServiceWorker.

So, what exactly is a ServiceWorker?

ServiceWorker is a script, running in the background (as stated before — in its own thread of execution), that acts as a programmable network proxy between the web application and the server. It is capable of caching static assets (think of lodash or mobile Twitter examples), as well as intercepting network requests, which we’ll talk about next.

ServiceWorker as the request interceptor

So far we’ve seen web applications which showed up in the browser, even though we were offline. But what if the connection gets lost while we try to interact with the server (eg. by sending form data, or trying to save the work-in-progress in an online editor)? In most web applications, we would either be presented with loading animation running infinitely, or — at best — with a notification saying that something went wrong.

ServiceWorker, however, enables us to intercept the request, look “inside” by reading its details (HTTP method, target URL, data payload) and either pass it through, or hold it, returning a substituted response instead.

For this article, I’ve created a small demo application. When the application is running, try hitting the “send data!” button. Next to the “result” label, a 200 OK message should appear:

Yay, we sent data to the server!

On the server side, a log of received data is printed, as expected:

Yup, got it.

Nothing interesting. The real magic happens when you go offline — if you already forgot how to do it, here’s a remainder screenshot from Chrome’s developer tools:

Let’s now pretend we lost the connection.

Let’s click the “send data! now:

So you say the request was not lost…?

Now that looks interesting, doesn’t it? The application is telling us that it detected the loss of connection, but our request did not go into the void. In the meantime, the output of the server remains intact — only one request received so far.

Let’s now go online again — uncheck the “offline” box in the developer tools (do not refresh the page, though!). After a short while, the application should detect that it is back online, and that’s when the cool stuff happens:

Nice, but how can I be sure?

The “missing” request finally made it to the server!

The buffered request eventually made it to the server. This opens up new possibilities for a plethora of modern web applications. Imagine using an online text editor travelling by train — while the changes made should be saved on the server periodically, many of these attempts may fail due to the poor connection, eg. in tunnels. As soon as it is possible, the application should “flush” the requests waiting in the queue.

While the logic in app.js file is mainly dealing with proper registration of the ServiceWorker (plus, setting up actions when the button is hit), I will skip the detailed code discussion. For thorough description of the registration process, I’ll send you to this great article by Jake Archibald. This article is already getting wordy, so I’ll highlight the most important pieces.

A ServiceWorker can listen to fetch events, which means all HTTP requests. An example (straight from the demo app) shows a very basic way of intercepting the request:

If event.respondWith is NOT called, the original request is passed through (to the server).

If the browser is offline, the request will be “stopped” and put in the buffer — additionally, the application will receive a “fake” response from the ServiceWorker (see line 6 of the gist: event.respondWith(...) ), so that we can pretend there was no error. Note that it is developer’s responsibility to implement a buffer; here I am using a very simple, transient solution (based on a plain old JavaScript object), but it could as well be a record in the IndexedDB database, so that requests can live in the buffer even after the browser’s tab is closed. The buffer then periodically checks whether the connection was restored — if so, it “flushes” all pending requests and passes them through to the server:

Part of the buffer’s logic. The retry function is executed periodically (every 5 seconds), checking whether the browser is back online.

See full code example of the ServiceWorker’s script from the demo app here.

With great power comes great responsibility…

…and that should be a mantra in case of ServiceWorkers.

The reality is that the lifecycle of ServiceWorker is quite complex and can cause a lot of headaches, if used incautiously. Mistakes in caching strategies or improper use of worker’s registration events can even result in your application’s users being “infected” with an old version of a ServiceWorker, serving the same cached version of the application all the time, making it virtually impossible to ever see updates you insistently try to propagate. While in many cases this could be easily fixed with few clicks in developer tools (Chrome) or at about:debugging tab (Firefox), remember that the majority of your users have no idea what developer tools are (plus, it’s slightly more difficult on mobile).

Just kill the bastard with “unregister” button on the right. But hey, not everybody knows what developer tools are.

A very popular Create-React-App added the ServiceWorker to the scaffolding a few months ago, quickly raising a dispute on whether it was a good decision. Many developers opposed that idea, and due to a nasty bug in the early version of ServiceWorker added to the boilerplate and problems with caching static files on localhost (in development mode), some were pulling their hair out of their heads really hard. After few months of discussion, they decided to make the ServiceWorker optional:

This great talk by Alexander Pope gives a detailed insight on how many things can go wrong when ServiceWorkers are used hastily:

Summary

This article has shown two use cases of ServiceWorker: offline availability and network requests interception. For detailed code examples I recommend reading MDN’s and Google’s great articles, but also a look at my demo application.

ServiceWorkers are still emerging technology. While the API specification is complete, they still remain unknown to many web developers. Maybe it’s because of the fact that Apple has never rushed for adding them to Safari (remember that Chrome on iOS is just re-skinned Safari browser). At the time of this writing (February 2018), ServiceWorker for Safari is marked as “in development”.

One doesn’t achieve the “offline-first” by simply throwing in a ServiceWorker and calling it a day — it’s rather a set of architecture design rules to follow. Weird behaviour might occur if the website is cached for offline use, but does not provide any mechanism for intercepting AJAX requests — this might result in half-broken app.

If used cautiously and properly, ServiceWorker brings great advantages. Misused — terrible consequences.

Links and references