- 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 | function renderApp() { ... } |
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 | function refill (applicationName, 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 | self.addEventListener('fetch', function (event) { |
You can even play with the bottle-service
features using the demo
at bottle-service.herokuapp.com 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 compatability, 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".