Instant Web Application

An instantly loading, self-rewriting application using ServiceWorker - it is like server rendering inside your browser.

  • Instant TodoMVC demo (please use Chrome Desktop for now!), source
  • Uses bottle-service library to implement self-rewriting

Open your favorite web application, even a simple TodoMVC would work. Let it load. Change some data, for example add a new item to the list. Now reload the page. What happens? The page goes blank, then some initial markup appears. Then all of the sudden, everything shifts - the application's code took over, rewriting the page's tree structure, forcing the browser to render the loaded data. Here is one example: the screen recording of Angular2 TodoMVC application where I add items and reload the page.

Before someone starts Angular-bashing, here is the screen recording of a React application, showing exactly the same problem

The vanilla JavaScript implementation has a better experience in my view, because only part of the page is updated (the items list), while the top stays static

Every application in the list suffers from the same problem - during the page reload there is a time gap between the initial page load and the application rendering the "right" HTML. Some libraries are faster (Mithril is great!), some are slower, but none approaches the server-side rendering for smooth user experience.

In server-side rendering, the page is rendered in the complete form on the server, thus when it arrives the user sees the right layout instantly. The web application can then take over, "hydrating" the static page. Some frameworks make such hydration simple, some might use my tiny hydration utility.

The larger question I want to answer is this:

Can we recreate the same "instant" page loading experience in our web application without the server-side rendering?

Instant web applications

Before we proceed, here is a screen recording of my TodoMVC implementation. You can try the live demo at instant-todo.herokuapp.com. There is no server, but it does require a modern browser supporting ServiceWorkers

Notice several things this web app has

  • Absolutely no flicker at page load. Only some small CSS effects (like check marks) appear once the web application takes over (I am using the virtual-dom library).
  • The state (the todo items) is stored in the localStorage, while the snapshot of the last rendered HTML is stored inside the ServiceWorker.
  • Every time the state changes, and the application has rendered itself, it sends the command to the ServiceWorker to store the serialized HTML text
  • When the browser requests the page again on reload, the ServiceWorker updates the fetched page with the HTML text.

This "instant" technology is called bottle-service; it is web framework-agnostic and should work with any library: Virtual-Dom, Angular, React, etc. The communication with the ServiceWorker part only has 1 API method, called refill. The application should call refill after the page has been rendered to save the snapshot.

Here is the application code that runs on every change to the data, you can see the full source in src/app.js

1
2
3
4
5
6
7
8
9
10
11
12
function renderApp() { ... }
function saveApp() {
localStorage.setItem(todosStorageLabel, JSON.stringify(Todos.items))
setTimeout(function () {
// application has renderd itself
// web application controls element <div id="app">
bottleService.refill(appLabel, 'app')
}, 0);
}
// on each user action
renderApp()
saveApp()

The method refill() is very simple - it just grabs the rendered HTML and sends it to the service worker to be stored. See its full code in bottle.js

1
2
3
4
5
6
7
8
9
10
function refill (applicationName, id) {
var el = document.getElementById(id)
var html = el.innerHTML
send({
cmd: 'refill',
html: html,
name: applicationName,
id: id
})
}

Let us look how the page's source is updated during the reload. This is the code inside the bottle-service service worker. Assume that HTML snapshot has been sent from the app at some point using bottleService.refill() and is available

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
self.addEventListener('fetch', function (event) {
// ignore everything but the request to fetch the index.html
event.respondWith(
fetch(event.request)
.then(function (response) {
// we have fetched the page from the server
var copy = response.clone()
// in order to rewrite the response we need to clone it
return copy.text().then(function (html) {
// find div with give id "app"
// replace with HTML snapshot
var updatedHtml = update(html, ...)
var responseOptions = {
status: 200,
headers: {
'Content-Type': 'text/html charset=UTF-8'
}
}
return new Response(updatedHtml, responseOptions)
})
})
)
})

You can even play with the bottle-service features using the demo at glebbahmutov.com/bottle-service/ where you can create new DOM nodes, print the HTML cached inside the ServiceWorker and clear the cached HTML.

Conclusion

In a sense, we have removed the need to render the application server-side (with its problems, framework compatibility, etc) and instead are using the best page rendering engine - the browser itself. Every time the state changes, the application needs to store both the state and the rendered HTML snapshot. The state can be stored inside the page, even inside the localStorage, while the HTML snapshot is sent to the ServiceWorker code where it will be available on page reload.

During page load, the ServiceWorker code is responsible for inserting the HTML snapshot into the fetched page, producing the complete page that the browser will see and render. Then the web application can take over. Of course, there is a delay between the page load and the instant it becomes it fully responsive application - but at least this is better than hiding the page behind the loading screens, or sudden violent page layout shifts.

Ideas for further research and experimentation

How much code does one need to load in order to make the first static page appear functional? Do you need the full framework + application code? Or can you just attach a couple of event listeners that will queue up all user commands to be executed once the application is fully loaded?

I want to explore dividing the library + web code into a tiny "above-the-fold" code fragment + the rest. We then can store the "above-the-fold" code together with the HTML snapshot in the ServiceWorker, loading it right away, making the application appear and respond to the user "instantly".