At Spring, we are responsible for maintaining various different news media websites for our parent company, Axel Springer. One of our latest releases, welt.de, is the fastest news media website in Germany; one of our most sought after goals is to continually achieve the best performance possible and the reason for this is simple: better performance usually means a better user experience and thus a higher user-retention.
Scroll down to “Recap” for a short summary with an infographic. The key points for this in a nutshell:
- Performance is crucial for the web
- to achieve high performance we want to send as little as possible to the client
- we can do this by choosing the components we want to send and hydrate to the client
- we leave the rest of the page static and have multiple render roots
- All of this works with a single code base
- Below is a lengthy article on how we implemented the steps noted above. You will also find a link to a WIP repo of this implementation here:
Performance on the web
Of course there is a lot more to performance than this, including loading strategies, a critical rendering path, performance budgets and so forth. All of these things revolve around how to optimize whatever you end up sending to your client. What we want to focus on with partial hydration is not how to optimize what you send, but how much you send at all.
A key aspect of this will be server side rendering (SSR) because there is a lot we can do on the server that does not need to be done on the client. In fact, this is the crux of this article; Whatever can be done on the server should be done on the server but the client should only be sent whatever needs to be executed on the client side. Additionally, you can still apply everything you know about web performance but you’ll have far fewer factors to manage, this will be explained in depth further on down the article.
SSR and hydration
In order carry out our objective of building a performant website we will be using a modified version of Next. Next comes bundled with plenty of built in performance enhancing features, most importantly, Next does server side rendering (SSR) out of the box. This means that Next will take your app, written in React and in this order:
- Render it as an HTML string on the server
- Send the rendered HTML string to your users as source code
- And then finally “hydrate” your HTML using your React code
To “hydrate” in this case means that Next will roll out your React code over your HTML and then tell React something a little like this:
Hey React, here’s some HTML that matches exactly what you would render if I told you to render in an empty DOM node, please do not re-render everything, instead, please just use the HTML as if you had rendered it and carry on with your day
React will answer
Ok, I just looked at you HTML and it seems to match exactly what I would have rendered. It’s cool. I’ll just attach some event handlers to your DOM and your page now acts as a single page application like I did it all myself in the first place.
The benefits of loading a website in this way are simple: Your users will already see a fully rendered page when they load your website (instead of a blank page) and then it becomes interactive. Your website will also benefit from a major performance improvement because the browser does not need to do any repaints (re-rendering your page with React).
Too much overhead
But most websites aren’t like this, most websites are kind-of-static and also contain some interactive elements.
Now you end up sending your entire application code to your users including React components for every headline or text paragraph anywhere on your page. The result is an unnecessarily huge bundle that needs to be loaded, parsed and executed. This results in suboptimal performance, your page will be slow(er) especially for mobile users and for no good reason!
And that sucks.
So what can we do? Well, there are lots of strategies and products out there. One of the most prominent ones is Gatsby, a static site generator (I’m sure you’ve heard of it by now) which focuses heavily on performance optimization. The issue with Gatsby is that it has to generate all of your pages and subpages at compile time which does not really work when you have sites linked to a CMS that updates everyday and that hosts millions of articles—which is exactly what we need for our news media sites. This is why we are using Next as well as modifying it to suit our needs.
Enter partial hydration
To solve the above mentioned problems we came up with something we like to call partial hydration.
If you have looked into this topic you probably will have come across terms like progressive hydration or lazy hydration. They don’t all mean the same thing specifically but they are all interrelated and belong in the same sphere.
You can also choose to implement proper performance strategies on top of all of this, like code-splitting, caring for the TCP slow start and your critical rendering path etc.
Our implementation consists of 2 packages:
- The Preact library for partial hydration called pool-attendant-preact
- The Next.js plugin called next-super-performance
The latter library is just a plugin for Next.js that uses pool-attendant-preact so let’s concentrate on pool-attendant-preact. I may write a followup post on next-super-performance at some stage in the future.
An example implementation for this using pool-attendant-preact could look like this:
Lines 10 & 11 are where we mark the components TwitterFeed and Poll for hydration and get a new component in return. Lines 18 & 19 are where we use them.
Line 22 is of utmost importance. This is a component that injects hydration data (props and component names) into the page.
But let me explain. When we do a normal hydration with react, your code looks something like this:
There are 2 problems we need to solve here when we do partial hydration
ReactDOM.hydrateoperates on a root node in the DOM, the node that it uses as a starting point for the hydration. That root node must contain a server-rendered DOM tree that matches your app’s components and state. The catch: You need to explicitly name a DOM node to act as a root node. In this example, this is simple, you can give that node an
document.getElementbyIdand then throw that node into
ReactDOM.hydrateand 🎉 you’re done!
Partial hydration on the other hand means that you will have multiple DOM elements on your static page that you need to hydrate. You wouldn’t want to explicitly name them all that would be tedious work for the developer.
- What if
HydratedPollneed props that need to be passed on to them? Say, something like
<HydratedTwitterFeed user="luke_schmuke" />. If we want to run
ReactDOM.hydrate(<TwitterFeed user="luke_schmuke" />, rootElementOnThePage)where would we get the
luke_schmukefrom? How would a static page know about this? We need to somehow store and send them to the client.
The way we tackled this problem can be understood from the implementation of withHydration:
Let’s take a closer look at this:
withHydration works using the higher order component technique, the higher order component returns the original component along with its original unaltered props but it also prepends it with a
<script> tag called
hydration-marker. This solves our 1st problem by finding our render roots in our server side rendered page. It works like this, the server will use the HOC (higher order component) to render something like this
<div class="twitter-feed"> is passed down from the
TwitterFeed component. The
script[type="application/hydration-marker"] and assume that any element directly following the script should be hydrated. It’s also possible to use wrapper elements but that would be too intrusive and it could potentially break your layout.
To address problem #2 let’s take another closer look at the code, lines 5 and 8 of withHydration.js. In line 5 we store the props passed to
HydratedTwitterFeed. In our initial example we passed no props at all but if we did something like
<HydratedTwitterFeed user="luke_schmuke" /> we would now have the following object stored:
Now our hypothetical client.js could hydrate our component using the DOM element adjacent to our marker using the props that we stored earlier.
But hey, how does the client.js access the data we stored earlier?
This is when
<HydrationData /> comes into play. When using this component all the data we have stored using
HydrationData.storeProps will be rendered as serialized JSON to the HTML. This process mimics how Next.js renders
NEXT_DATA and how Apollo renders
Also note that calling
HydrationData.storeProps will return and ID that we then pass on to the hydration marker, this way we can map the data in
<HydrationData /> to its corresponding markers and DOM elements
This is how the hydration part on the client would work:
- First, find all markers on the page
- Next, find the hydration data and load it
- And finally, iterate over all the markers and get their subsequent DOM elements and hydrate them using Preact’s
You may have noticed that in this example we have a component called
Teaser which is used to hydrate all of our elements. In our
super-intendant-preact module we expose a function called
hydrate which enables you to create a component like
Teaser to hydrate multiple components.
There’s a lot of information to take in from this article, my purpose for writing about the subject was to illustrate our particular method of using partial hydration and to explain how it works. If you would like to dive deeper into the code behind this article then please have a look at our GitHub repository:
Here’s a helpful infographic to help you visualize our partial hydration method a little better:
*Our approach to using partial hydration works quite well, we have already used a similar method with Vue which has proven to be successful. I expect to see more written about this topic in the future, some people within the web development community have been working on their own partial hydration methods, the developers behind React have begun to take notice of this so we may see some partial hydration features being incorporated into React’s core in the future.
We like to ship as little code as possible to ensure that we end up with more performant websites which in turn deliver a better user experience, after all, the smaller the codebase the smaller the bundle the user ends up having to deal with. Partial hydration enables us to do this without compromise while using the tooling and eco system of modern web development.