Optimizing React SSR Performance : Part I

uzumakinarut0

Kumar Swapnil

Posted on June 13, 2020

Optimizing React SSR Performance : Part I

Without burdening you with the intricate details of the cost of Javascript, anywhere up to 30% of the page load time can be spent in JavaScript execution.

It can create an annoying User Experience if the main thread is busy for long duration rendering the site unresponsive.

In React, when we render a page server side, we get the benefit of a better First Contentful Paint where the user does not have to wait for the Javascript to boot up and render the page. But, we still need to hydrate in order to make the page interactive. This Client side hydration is slow and can get really slow if there are a lots of components on the page.

To tackle this, strategies like Progressive Hydration and Selective Hydration is already on the React roadmap and I hope we get to use these awesome strategies real soon. But for the time being, I have tried to implement a pattern known as idle-until-urgent which basically helps to break the hydration from one long task into smaller tasks which can be executed during Browser’s ideal periods or When the user interacts with it. Okay, enough with the words, let’s see some code.

import React from 'react';

export function idleUntilUrgent(WrappedComponent, ComponentId) {
    class IdleUntilUrgent extends React.Component {
        constructor(props) {
            super(props);
            this.renderChild = false;
            this.firstRender = true;
            this.callbackId = null;
        }

        shouldComponentUpdate(nextProps, nextState) {
            return (
                this.props != nextProps || (nextState && nextState.renderChild)
            );
        }

        // to prevent calling setState on an unmounted component
        // and avoid memory leaks
        componentWillUnmount() {
            this.callbackId && cancelIdleCallback(this.callbackId);
        }

        enqueueIdleRender = () => {
            if (typeof requestIdleCallback !== "undefined") {
                // https://caniuse.com/#search=requestIdleCallback
                this.callbackId = requestIdleCallback(() => {
                    const root = document.getElementById(ComponentId);
                    this.setState({
                        renderChild: true
                    });
                });
            } else {
                setTimeout(() => {
                    const root = document.getElementById(ComponentId);
                    this.setState({
                        renderChild: true
                    });
                });
            }
        };

        urgentRender = () => {
            this.setState({
                renderChild: true
            });
        };

        render = () => {
            if (typeof window !== "undefined" && this.firstRender) {
                this.firstRender = false;
                this.enqueueIdleRender();
                return (
                    <div
                        dangerouslySetInnerHTML={{ __html: "" }}
                        suppressHydrationWarning={true}
                        onClick={this.urgentRender}
                    />
                );
            } else {
                // Cancel the already scheduled render, if any
                this.callbackId && cancelIdleCallback(this.callbackId);
                return <WrappedComponent {...this.props} />;
            }
        };
    }
    const wrappedComponentName =
        WrappedComponent.displayName || WrappedComponent.name || "Component";
    IdleUntilUrgent.displayName = `IdleUntilUrgent (${wrappedComponentName})`;
    return IdleUntilUrgent;
}

Let’s understand the above code one byte at a time:

  • During the hydration phase, we use this neat trick of passing an empty string to dangerouslySetInnerHtml which will bail the component out of the hydration phase (React doesn’t try to manipulate the tree of a dangerouslySetInnerHTML node on the client. Even if it is wrong.), thus saving us the hydration cost for the component.
    Also, it will call enqueueIdleRender which will render the component later at some idle time.

  • Before the component is interactive, if a user interacts with it, it will render immediately (making it interactive) and in this process will cancel the already scheduled render to avoid multiple renders.

  • This way, we can simply wrap the components with this Higher Order Component in order to split the hydration cost into multiple smaller tasks rather than one long task keeping the app responsive.

Results: With this approach, the initial hydration cost dropped by ~45%, from ~128 ms to ~70ms. These are really impressive results and the difference and benefits will only grow up when the Components will grow on the page.

  • Before Idle-Until-Urgent
    before Idle-Until-Urgent

  • After Idle-Until-Urgent
    after Idle-Until-Urgent

  • Conclusion: This is one of the ways we can make progressive enhancement to our page to improve the overall User Experience. This will help you bring down the Maximum First Input Delay and Total Blocking Time of the page. So, hopefully, this post will convince you to think about the way you were hydrating your react app until now.

💖 💪 🙅 🚩
uzumakinarut0
Kumar Swapnil

Posted on June 13, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related