Web Performance Calendar

The speed geek's favorite time of year
2016 Edition
ABOUT THE AUTHOR

Gleb Bahmutov (@bahmutov) is JavaScript ninja, image processing expert and software quality fanatic. After receiving a PhD in computer science from Purdue University, Gleb worked on laser scanners, 3D reconstruction, and panorama-based virtual tours at EveryScape. Later Gleb switched to writing browser data visualization software at MathWorks. After a year, Gleb went back to the startup environment and developed software quality analysis tools at uTest (now Applause). Today Gleb is developing real-time financial analysis tools at Kensho. He blogs about software development topics at https://glebbahmutov.com/blog/ and links his projects at http://glebbahmutov.com/.

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

Important links:

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

The problem

Open your favorite web application, even a simple TodoMVC web application. 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. For example, see https://todomvc-express.gleb-demos.com/ – this server-rendered page appears instantly as a single entity.

The question I want to answer is:

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

 

Several important points

  • I delay the web application bootstrap on purpose (there is even a popup message when the web application takes over)
  • The page shows absolutely no flicker during load. Only some small CSS effects (like check marks) appear once the web application takes over.
  • 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. Thus the browser has the correct page to load next time.
  • 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: Vue, 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

function renderApp() { ... }
function saveApp() {
  // save the data
  localStorage.setItem(todosStorageLabel, JSON.stringify(Todos.items))
  setTimeout(function () {
    // application has renderd itself
    // web application controls element <div id="app">
    // save the DOM snapshot
    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

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

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.