Deep dive into the new Suspense Server-side Rendering ( SSR ) architecture in React 18

The much anticipated React 18, (now in beta) is on the horizon and offers a new Suspense SSR Architecture.

To understand the new architecture, one must be familiar with the basic concepts like client-side rendering, server-side rendering, hydration, etc.

We have explained these concepts in our previous blog post related to Hydration. It is recommended to go through it before jumping to the new architecture.

How does SSR work?

In SSR, data is fetched, and the HTML is generated from the React components on the server. The HTML is then sent out to the client.

Here are the steps which are followed during SSR:

  1. Data is fetched for the entire application on the server
  2. The HTML is generated for the entire application from the React components on the server, which is then sent to the client.
  3. On the client ( browser ), the JavaScript code is loaded for the the entire application
  4. The JavaScript logic is then connected to the server-generated HTML for the the entire application. This process is called Hydration. It makes the site interactive.

The image is taken from a talk by Shaundai in React Conf 2021.

Notice that we have emphasized the entire application in each step. This is because each step had to finish for the entire app at once before the next step could start. This is not efficient if some parts of the application are slower than others.

Let’s consider the example mentioned in the SSR WG discussion. Here, our application has NavBar, SideBar, and RightPane containing Post and Comments.

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Comments />
  </RightPane>
</Layout>

‘Comments’ part is the most important part of our application in which users are interested. But, let’s say the <Comments> component involves expensive API request for a large amount of data and has a lot of JavaScript logic involved.

Now, let us look into the SSR issues for this application.

What are the problems in SSR before React 18?

1. Fetch everything before showing anything

As we have seen earlier, we need to fetch all the data before showing anything to the user. This means we also need to fetch all the comments which might take time for a large amount of data. It is inefficient, as a user will not see anything on the screen.

Now, we are left with two choices -

  1. Delay sending the HTML from the server
  2. Exclude comments from the HTML. This would create an overhead on a client to render comments.

Both these options do not look good.

2. Load everything before hydrating anything

We know that all the JavaScript code needs to load before starting to hydrate. Again, our <Comments> component has a lot of complex JavaScript logic involved, it would take some time to load.

Even though the JS code for NavBar, SideBar, Post is loaded, hydration cannot start.

Again, we are left with two choices -

  1. Delay hydrating till all the JS code is loaded. But this is not ideal.
  2. Use code-splitting for Comments and load it separately. This means we have to exclude comments from the server HTML. Otherwise, React won’t know what to do with this chunk of HTML and throw it away during hydration.
3. Hydrate everything before interacting with anything

Let’s say, our <Comments> component has an expensive rendering logic, which takes a while to attach event handlers. As we know, hydration takes place in a single pass. This means once it starts hydrating, React won’t stop until it is finished. As a result, we have to wait for all components to be hydrated before we can interact with any of them.

Consider a case where a user clicked on a post by mistake. Now, he wants to navigate to the home page. The application is frozen due to the ongoing hydration. Due to this, the user cannot navigate, even if the Home page link is visible in the NavBar.

What a waste of time for the user!

Solution

Thanks to the new Suspense SSR architecture in React 18, which provides a solution to all the problems!

We break the work, instead of following the waterfall model -

Fetch data (server) → Render to HTML (server) → Load JS code (client) → Hydrate (client)

It enables us to follow each of these stages for a part of the screen instead of the entire app.

Let’s look into this in more detail.

Streaming HTML and Selective Hydration in React 18

Suspense is something that lets us ‘wait’ for some code to load and specify a loader while we are waiting for the code to finish loading.

There are two major SSR features in React 18 unlocked by Suspense:

  1. Streaming HTML on the server:

    To opt into it, we need to switch from renderToString to the new renderToPipeableStream method.

  2. Selective Hydration on the client:

    To opt into it, we need to switch to createRoot on the client and then start wrapping parts of our app with <Suspense>.

Sticking to the example discussed earlier, we know that the <Comments> component is a problem creator. So let’s wrap it into <Suspense> and tell React that until it’s ready, React should display the <Spinner /> component:

Streaming HTML before all the data is fetched
<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

This way, we tell React to not wait for comments and start streaming HTML for the rest of the application. Comments would be replaced by Spinner placeholder.

 <main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>

When the data for the comments is ready on the server, React will send additional HTML into the same stream, along with the <script> tag to put that HTML in the ‘right place’.

Even before React itself loads on the client, the HTML for comments will be loaded.

How cool is that!

This is called ‘Streaming HTML’. This is how we solve the first problem we discussed earlier- Fetch everything before showing anything.

Hydrating the page before all the code has loaded

By wrapping Comments in <Suspense>, we not only tell React to unblock the rest of the page from streaming but also from hydrating!

This is called ‘Selective Hydration’. Thanks to Selective Hydration, a heavy piece of JS doesn’t prevent the rest of the page from becoming interactive.

In the below image, we see that <Suspense> lets us hydrate the app before the <Comments> component has loaded.

React then starts hydrating the Comments section after the JS code is loaded.

This way our second problem is solved- Load everything before hydrating anything.

Hydrating the page before all the HTML has been streamed

One more benefit from wrapping Comments in <Suspense>, is that hydration no longer blocks user interactivity!

In the below image, we see that we are able to click on the SideBar, even when the <Comments> component is hydrating.

When the JavaScript code for the Comments loads, the page will become fully interactive.

Prioritzing Hydration

Suppose, we have multiple components wrapped in <Suspense>.

<Layout>
  <NavBar />
  <Suspense fallback={<Spinner />}>
    <Sidebar />
  </Suspense>
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

React will attempt to hydrate both of them, starting with the Suspense boundary that it finds earlier in the tree ( SideBar in this case ).

Let’s say the user starts interacting with the comments section, for which the code is also loaded. In this case, React will prioritize hydrating the comments assuming it to be more urgent and makes the comment section interactive. After that, it will continue hydrating the Sidebar.

This solves our third problem - Hydrate everything before interacting with anything

These under the hood improvements in the <Suspense> component have solved a lot of SSR issues. Thanks to the React team for doing so much work on Suspense!

More details about these changes can be found in WG discussion.

Need help on your Ruby on Rails or React project?

Join Our Newsletter