loading...
Cover image for Optimizing React SSR Performance: Part II

Optimizing React SSR Performance: Part II

uzumakinarut0 profile image Kumar Swapnil Updated on ・3 min read

Optimizing React SSR Performance (2 Part Series)

1) Optimizing React SSR Performance : Part I 2) Optimizing React SSR Performance: Part II

In the earlier post, we have seen how we can split the JavaScript execution cost from one long running tasks to multiple smaller tasks and improve the perceived performance of the page. Now, this approach can be fine tuned further by leveraging Code Splitting.
Code-Splitting is a feature supported by bundlers like Webpack which allows you to create smaller chunks which you can then “lazily load” on demand. React provides React.lazy to render a dynamic import as a regular component. Let us look at code below,

const LazyComponent = React.lazy(() => import('./LazyComponent'));

This will create a different chunk for LazyComponent. Now, we can use this to lazy load our components straight away, but there is a catch. React.lazy and Suspense are not yet available for server-side rendering. They recommend loadable components to Code Split our app this way if we want Code splitting with server rendering.

Now, let’s recap the two aspects which we will be focusing on:

  • We can bail out of hydration phase using the dangerouslySetInnerHtml trick.
  • We can lazy load our chunks using Code Splitting with loadable components.

Now, the question is, when do we fetch these chunks and hydrate the Component. One possible solution is, just before they are entering viewport. This is referred as Progressive hydration. In Google I/O ’19, we get a gist of what it is and how we can implement this today (they did mention about React’s roadmap to ship this). So, let’s look at the code as how can we achive it.

import React from 'react'
import ReactDOM from 'react-dom'
import loadable from '@loadable/component';
import { InView } from "react-intersection-observer";

/**
 *
 * @description lazily loads the JS associated with a component and hydrates it based on its distance from viewport
 * @tutorial {@link https://youtu.be/k-A2VfuUROg?t=960} - Rendering on the Web: Performance Implications of Application Architecture
 * @tutorial {@link https://medium.com/@luke_schmuke/how-we-achieved-the-best-web-performance-with-partial-hydration-20fab9c808d5} - Partial Hydration
 */
class Hydrator extends React.Component
{
    constructor(props)
    {
        super(props);
        this.Child = null;
        this.state = {
            renderChild : false
        }
    }

    shouldComponentUpdate(nextProps, nextState)
    {
        return nextState.renderChild;
    }

    handleInviewChange = async (inView) => {
        if (inView) {
            const {load, id} = this.props;
            const root = document.getElementById(id);
            this.Child = loadable(load);
            this.setState({
                renderChild : true
            });
        }
    };

    render()
    {
        if(typeof window !== "undefined")   // Avoid client side hyration
        {        
            if(this.state.renderChild)
            {
                return React.createElement(this.Child, this.props);
            }
            else
            {
                return (
                    <InView
                        rootMargin="640px"  // to avoid janky experiency in case the user scrolls fast
                        triggerOnce={true}
                        dangerouslySetInnerHTML={{ __html: "" }}
                        suppressHydrationWarning={true}
                        onChange={this.handleInviewChange}
                        {...this.props}
                    />
                );
            }
        }
        else    // Server side rendering
        {
            return(
                <div dangerouslySetInnerHTML={{__html: this.props.serverMarkup}}>
                </div>
            );
        }
    }
}

export default Hydrator;

Since the Server side rendering is going to happen in a single pass, you need to handle your component’s markup differently on the server side. You can simply create the component independently and the pass this as a prop to the root component. The Code would look something like below:

private string SSR(componentProps, progressiveComponentProps = null)
{
    componentProps.ProgressiveComponent  = RenderProgressiveComponents(progressiveComponentProps);            
    var reactComponent = Environment.CreateComponent(componentName, componentProps, containerId, clientOnly);
}

private static string RenderProgressiveComponents(componentProps)
{
  IReactComponent reactComponent = Environment.CreateComponent("LazyCarousel", componentProps, "article-detail__carousel-wrapper");
  return reactComponent.RenderHtml();
}

So, once we handled the Server rendering (and else block of render method). Let’s have a closer look at the if block of render() method now. When the first time render() will be called on client side, it will wrap the component with InView and will make use of dangerouslySetInnerHtml to avoid hydration cost. The InView component makes use of Intersection Observer to observe an element’s visibility.
Now, once the user is about to enter viewport, the handleInviewChange will trigger which will fetch the component chunk making good use of loadable and then toggle the component state to trigger rendering. This way, now the component is ready to handle user interactions.

Results: In addition to what we achieved from previous post, now we are able to save on initial network cost to boot up the app. Here, along with Network cost, we are also saving on the JavaScript parse /compile time which was unavoidable in the last implementation.

Conclusion: In my opinion, both idle-until-urgent and progressive hydration can complement each other to build a better progressive web app.
While Progressive hydration makes sense on below the fold components which can significantly (like carousels) reduce the initial chunk size required to boot the app but it comes up with an overhead of special Server Side handling, on the other hand Idle-Until-Urgent is pretty quick and easy to implement(making it a better candidate for light weight components).

Optimizing React SSR Performance (2 Part Series)

1) Optimizing React SSR Performance : Part I 2) Optimizing React SSR Performance: Part II

Discussion

markdown guide