Hydrate at build time

Generate the initial HTML markup from web app at build time to avoid blank screen.

demo, source

I have shown how to quickly restart a web application from last known markup saved in localStorage in Hydrate your apps blog post. In this blog post I will show how to do the same at the very first time the web application loads. We are going to use the web application itself to generate the initial HTML markup during the build step, and then we are going to "rehydrate" the markup when the application starts in the user's browser. This avoids a flash of a blank content or a spinning loader common in many web apps.

Typical web application startup

Look at a typical web application startup timeline below (you can see this live)

web application

The browser loads the initial base page, then the framework and application code. Once the web application starts it fills the blank page with the actual content. The user sees blank space until the web application starts. If we inspect the page's markup, it is empty at the beginning

1
2
3
4
5
<body>
<div id="app"></div>
<script src="framework.js"></script>
<script src="app.js"></script>
</body>

The application fills the "div#app" element with the actual content

1
2
3
4
5
6
7
8
9
<body>
<div id="app">
<section class="todoapp">
<header class="header">...</header>
<section class="main">...</section>
<footer class="footer">...</footer>
</section>
</div>
</body>

Can we do better? In some cases, we do know the initial data that our application will render at build time. Let us prebuild the HTML markup and serve it right away, and the web application will have time to start and then enhance the static markup with live behavior (I call this enhancement "hydration")

Prebuilt web application startup

If we are showing a Todo application with a set of initial items, we could run the web application at build time (some frameworks make it easier than others, for example virtual-dom makes it very simple), capture the produced HTML and put it into the "index.html".

You can find this source code in bahmutov/hydrate-vdom-todo. We are going to start by looking at the web application main rendering loop - and it is very simple.

See the full file at src/app.js

1
2
3
4
5
6
7
8
9
10
11
12
const Todos = require('./todos') // data = Model
const render = require('./render/render') // Model -> VirtualNode
// standard Virtual-Dom rendering loop
var prevView = render(Todos) // VirtualNode
var renderedNode = createElement(preView) // initial DOM element
document.body.appendChild(renderedNode); // add it to the document
function renderApp () {
const view = render(Todos) // view is a VirtualNode
const patches = diff(prevView, view) // prevView is previous VirtualNode
renderedNode = patch(renderedNode, patches) // (DOM node, patches) -> (updated Dom node)
prevView = view
}

This is pretty standard virtual dom rendering loop. The great thing about the first 3 code lines - they do not require a live browser and produce "virtual" markup from actual data, and we can easily convert it to HTML at build time.

Thus as part of my build in addition to running webpack, I also run hydrate-app.js that is extremely simple. It takes the empty index.html and fills the web application's div with the initial HTML

1
2
3
4
5
6
7
8
9
10
11
12
// Model
const render = require('./src/render/render')
const Todos = require('./src/todos')
// rendered is a VirtualNode
const rendered = render(Todos)
const toHTML = require('vdom-to-html')
const beautify = require('js-beautify').html
const appMarkup = beautify(toHTML(rendered), { indent_size: 2 })
// appMarkup is the same as if running in the browser
// insert the markup into "index.html", saving the result
// in "dist/index.html"
updateIndex('./index.html', './dist/index.html', appMarkup)

The dist/index.html has nice static HTML in its application div

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div id="app">
<section class="todoapp">
<header class="header">...</header>
<section class="main">
<input class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
...
</ul>
</section>
<footer>...</footer>
</section>
</div>
</body>

The browser now shows the correct information immediately.

Hydrating the page

We now get the initial web application markup immediately because it is part of the page. How do we "hydrate" or tell our application that it should just extend the existing markup? By default, the rendering loop tries to create a new root element from the VirtualNode

1
2
3
4
// start render
var prevView = render(Todos) // VirtualNode
var renderedNode = createElement(preView) // initial DOM element
document.body.appendChild(renderedNode); // add it to the document

Instead of using createElement that takes VirtualNode instance and returns new DOM element, we can grab the DOM element from the page and create a VirtualNode! I am using helper library html-to-vdom that does this.

1
2
3
4
5
// grab DOM element already present
const appNode = document.getElementById('app')
var renderedNode = appNode.firstElementChild
const convertHTML = require('html-to-vdom')
var prevView = convertHTML(renderedNode.outerHTML)

That is it, now that we created a VirtualDom from the actual HTML we can patch it using normal VirtualDom diff methods.

1
2
3
4
5
6
function renderApp () {
const view = render(Todos)
const patches = diff(prevView, view)
renderedNode = patch(renderedNode, patches)
prevView = view
}

This is how a Virtual-Dom application can start without destroying and recreating a part of the page.

Results

You can check out the live result, and most importantly profile the timeline.

hydrated timeline

Notice that the Todo contents is shown right away - the application code is non-blocking at the bottom of the body, the markup is available, and the user sees the list at startup. The application once again starts at 1 second mark, but the content does not flash or flicker - because the virtual dom only adds a few patches to the HTML already there; all the patches are just setting up the event handlers!

A note of caution

The true web application should still start quickly. The initial markup might be there, but it is static - there are no event handlers, and the user might try to interact with the page only to find that it is unresponsive. One can allow the page to be used in the read-only mode - the user should be able to scroll, maybe go to the outside resources via A links, and so on.

Further reading

Some frameworks make it simple to render HTML and state (model) to hydrate the page and quickly start. For example Virtual-Dom and React make it simple to generate static HTML on the server and hydrate on the client.

React implementation

You can see how React can generate custom HTML markup and hydrate the application at every server request in this universal example. Each HTTP GET to the page runs the application code and generates 2 things: the HTML and the data store.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.use(handleRender)
function handleRender(req, res) {
...
const store = configureStore(initialState)
// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)
// Grab the initial state from our Redux store
const finalState = store.getState()
// Send the rendered page back to the client
res.send(renderFullPage(html, finalState))
}

We send both the HTML and the finalState; the state being a simple JSON object in the page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function renderFullPage(html, initialState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="app">${html}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`
}

The client code can grab the state and update the HTML to start running

1
2
3
4
5
6
7
8
9
const initialState = window.__INITIAL_STATE__
const store = configureStore(initialState)
const rootElement = document.getElementById('app')
render(
<Provider store={store}>
<App/>
</Provider>,
rootElement
)