Improving Web Performance with React Hydration Strategies

Photo by Nick Sorockin on Unsplash

With 23 million unique visitors per month, Cdiscount is the French leader in e-commerce, and as a result, we continuously strive to achieve the best possible performance. The reason is very simple: better performance usually means a better user experience and therefore a higher user retention rate.

Google announced in May 2020 that it will prioritize sites based on Core Web Vitals (LCP, FID, and CLS). As a result, performance is no longer just about user experience, but also about SEO ranking.

In this article we will see how the implementation of progressive hydration strategies helped us to reduce the First Input Delay on our mobile site by more than 50%, allowing us to reach the “FAST” goal on all three indicators.

Hydration and its impact on the FID

With a strong SEO focus and highly cacheable content, server-side rendering is an obvious choice for an e-commerce website like Cdiscount.

Despite server rendering allowing users to view our website quickly in their browsers, the JavaScript bundles must still be loaded, processed, and executed for them to interact with it. In this process, called hydration, React checks the nodes in the current DOM and hydrates them with the corresponding JavaScript by creating what is called the Virtual DOM.

The whole page is hydrated all at once, meaning the user must wait until the bottom of the page is hydrated before they can interact with the top of the page. This can be frustrating for the user, as the UI can appear frozen.

So how exactly does hydration impact FID? Long first input delays are typically caused when a user tries to interact with the page while the main thread is busy and unable to respond right away. On a server-side rendered page, the hydration process can take up a lot of space on the main thread.

To improve the FID as well as the user experience, our goal is to reduce and split the work on the main thread into smaller tasks so that the user can start interacting with the page sooner and that the browser can handle these interactions as quickly as possible. That’s where hydration strategies come into play.

Active hydration

Active hydration consists of hydrating only the dynamic parts of the DOM. Instead of a single entry point being responsible for hydrating the whole page, there are multiple entry points, one for each dynamic element which can be delivered and hydrated independently, leaving the rest of the page static HTML. This is similar to an islands architecture.

While active hydration is primarily for pages with a lot of static content, and it is not easy to implement on an existing app, our current architecture allowed us to use a similar approach.

The first step was to split each hydration into its own task using a well-placed setTimeoutand a dynamic import with Webpack’s magic comment webpackMode: "eager". You may be wondering why this was not already the case, since each of our three parts has its own entry point and file? Well if the right conditions are met, it appears that browsers can parse/execute different scripts within the same task.

Then the use of IntersectionObserver and dynamic import allowed us to differ the code download and the hydration of the footer right before the user scrolls it into view.

With these two steps, we were able to reduce our Total Blocking Time by over 40%.

With the footer removed from our page’s initial load, let’s look at how we can optimize our header and body.

Partial and Progressive hydration

The active hydration strategy you can see above works best when you want to target some elements to be interactive. Consider the opposite scenario: we have a mostly interactive application, but we want to exclude certain elements from the hydration step because they are not interactive. This is referred to as Partial Hydration.

Alternatively, we may want to trigger hydration at a later point, for example, to wait until an element is visible. Or we may just want to delay the hydration of less important parts of the page. This is called Progressive Hydration.

Let’s see how partial and progressive hydration was used to improve the performance of the Cdiscount mobile site:

Landing page

Due to the simplicity of the components, mostly composed of images and links, we were able to skip hydration for most of them. However, we were not able to remove any client-side code since only the first 10 elements are rendered server-side, with the remaining elements being rendered client-side. With partial hydration, we were able to reduce our landing page hydration time by as much as 80%.

Product page

The product page is the most visited template and the one which had the highest FID. There is a lot of content located below the viewport and it turns out that the scroll rate is very low, which led us to hydrate a large number of these components on demand while downloading their code dynamically. These optimizations enabled us to reduce our maxFID by 40% and our bundle size by 30%, while our users can interact with the top of the page way sooner.

Implementation

The implementation we use at Cdiscount is available on Github:

At the time of this article, this library has more than 300K npm installs per month and is used on multiple high-traffic websites.

To Hydrate or Not To Hydrate

That is the question ;). Even if splitting the hydration task into smaller tasks makes the browser respond to user inputs more quickly, it takes a few moments for the browser to check its input event queue and pick up the next task. Moreover, by using progressive hydration we skip the hydration step and then call React.render, which is more costly than simply hydrating the component with the rest of the tree. All of this can delay the time at which an element is fully interactive.

To eliminate the need to make the trade-off between load performance and input responsiveness, Facebook (not a coincidence) proposed and implemented the isInputPendingAPI in Chromium.

As the name implies, isInputPending tells you whether there is input pending. Developers can then use this information while running JavaScript to decide whether they want to yield back to the browser. When used properly, isInputPending can completely eliminate the trade-off between loading quickly and responding to events quickly.

When using progressive hydration, this allows us to skip hydration to let the browser handle a user interaction or to hydrate everything in one go if there is no user interaction in the queue.

isInputPending has been available since the release of Chrome 87 and it is already possible to benefit from it in the lastest release of react-hydration-on-demand.

Conclusion

In the end, implementing hydration strategies has proved to be highly effective and has played a critical role in improving our mobile site’s performance. By moving all three core web vitals to FAST, Cdiscount has seen a positive impact on its users and business.

We are looking forward to the release of React 18 which will allow us to combine server-side streaming rendering with a new approach to hydration: selective hydration.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store