A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/reactwg/react-18/discussions/37 below:

New Suspense SSR Architecture in React 18 · reactwg/react-18 · Discussion #37 · GitHub

Overview

React 18 will include architectural improvements to React server-side rendering (SSR) performance. These improvements are substantial and are the culmination of several years of work. Most of these improvements are behind-the-scenes, but there are some opt-in mechanisms you’ll want to be aware of, especially if you don’t use a framework.

The primary new API is renderToPipeableStream, which you can read about in Upgrading to React 18 on the Server. We plan to write more about it in detail as it's not final and there are things to work out.

The primary existing API is <Suspense>.

This page is a high-level overview of the new architecture, its design, and the problems it solves.

tl;dr

Server-side rendering (abbreviated to “SSR” in this post) lets you generate HTML from React components on the server, and send that HTML to your users. SSR lets your users see the page’s content before your JavaScript bundle loads and runs.

SSR in React always happens in several steps:

The key part is that 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 your app are slower than others, as is the case in pretty much every non-trivial app.

React 18 lets you use <Suspense> to break down your app into smaller independent units which will go through these steps independently from each other and won’t block the rest of the app. As a result, your app’s users will see the content sooner and be able to start interacting with it much faster. The slowest part of your app won’t drag down the parts that are fast. These improvements are automatic, and you don’t need to write any special coordination code for them to work.

This also means that React.lazy "just works" with SSR now. Here's a demo.

(If you don’t use a framework, you will need to change the exact way the HTML generation is wired up.)

What Is SSR?

When the user loads your app, you want to show a fully interactive page as soon as possible:

This illustration uses the green color to convey that these parts of the page are interactive. In other words, all their JavaScript event handlers are already attached, clicking buttons can update the state, and so on.

However, the page can’t be interactive before the JavaScript code for it fully loads. This includes both React itself and your application code. For non-trivial apps, much of the loading time will be spent downloading your application code.

If you don’t use SSR, the only thing the user will see while JavaScript is loading is a blank page:

This is not great, and this is why we recommend using SSR. SSR lets you render your React components on the server into HTML and send it to the user. HTML is not very interactive (aside from simple built-in web interactions like links and form inputs). However, it lets the user see something while the JavaScript is still loading:

Here, the grey color illustrates that these parts of the screen are not fully interactive yet. The JavaScript code for your app has not loaded yet, so clicking buttons doesn’t do anything. But especially for content-heavy websites, SSR is extremely useful because it lets users with worse connections start reading or looking at the content while JavaScript is loading.

When both React and your application code loads, you want to make this HTML interactive. You tell React: “Here’s the App component that generated this HTML on the server. Attach event handlers to that HTML!” React will render your component tree in memory, but instead of generating DOM nodes for it, it will attach all the logic to the existing HTML.

This process of rendering your components and attaching event handlers is known as “hydration”. It’s like watering the “dry” HTML with the “water” of interactivity and event handlers. (Or at least, that’s how I explain this term to myself.)

After hydration, it’s “React as usual”: your components can set state, respond to clicks, and so on:

You can see that SSR is kind of a “magic trick”. It doesn’t make your app fully interactive faster. Rather, it lets you show a non-interactive version of your app sooner, so that the user can look at the static content while they wait for JS to load. However, this trick makes a huge difference for people with poor network connections, and improves the perceived performance overall. It also helps you with search engine ranking, both due to easier indexing and due to better speed.

Note: don’t confuse SSR with Server Components. Server Components are a more experimental feature that is still in research and likely won’t be a part of the initial React 18 release. You can learn about Server Components here. Server Components are complementary to SSR, and will be a part of the recommended data fetching approach, but this post is not about them.

What Are the Problems with SSR Today?

The approach above works, but in many ways it’s not optimal.

You have to fetch everything before you can show anything

One problem with SSR today is that it does not allow components to “wait for data”. With the current API, by the time you render to HTML, you must already have all the data ready for your components on the server. This means that you have to collect all the data on the server before you can start sending any HTML to the client. This is quite inefficient.

For example, let’s say you want to render a post with comments. The comments are important to show early, so you want to include them in the server HTML output. But your database or API layer is slow, which is out of your control. Now you have to make some hard choices. If you exclude them from the server output, the user won’t see them until JS loads. But if you include them in the server output, you have to delay sending the rest of the HTML (for example, the navigation bar, the sidebar, and even the post content) until the comments have loaded and you can render the full tree. This is not great.

As a side note, some data fetching solutions repeatedly try to render the tree to HTML and throw away the result until the data has been resolved because React doesn't provide a more ergonomic option. We’d like to provide a solution that doesn’t require such extreme compromises.

You have to load everything before you can hydrate anything

After your JavaScript code loads, you’ll tell React to “hydrate” the HTML and make it interactive. React will “walk” the server-generated HTML while rendering your components, and attach the event handlers to that HTML. For this to work, the tree produced by your components in the browser must match the tree produced by the server. Otherwise React can’t “match them up!” A very unfortunate consequence of this is that you have to load the JavaScript for all components on the client before you can start hydrating any of them.

For example, let’s say that the comments widget contains a lot of complex interaction logic, and it takes a while to load JavaScript for it. Now you have to make a hard choice again. It would be good to render comments on the server to HTML in order to show them to the user early. But because hydration can only be done in a single pass today, you can’t start hydrating the navigation bar, the sidebar, and the post content until you’ve loaded the code for the comments widget! Of course, you could use code splitting and load it separately, but you would have to exclude comments from the server HTML. Otherwise React won’t know what to do with this chunk of HTML (where’s the code for it?) and delete it during hydration.

You have to hydrate everything before you can interact with anything

There is a similar issue with hydration itself. Today, React hydrates the tree in a single pass. This means that once it’s started hydrating (which is essentially calling your component functions), React won’t stop until it’s finished doing this for the entire tree. As a consequence, you have to wait for all components to be hydrated before you can interact with any of them.

For example, let’s say the comments widget has expensive rendering logic. It might work fast on your computer, but on a low-end device running all of that logic is not cheap and may even lock up the screen for several seconds. Of course, ideally we wouldn’t have such logic on the client at all (and that’s something that Server Components can help with). But for some logic it’s unavoidable because it determines what the attached event handlers should do and is essential to interactivity. As a result, once the hydration starts, the user can’t interact with the navigation bar, the sidebar, or the post content, until the full tree is hydrated. For navigation, this is especially unfortunate since the user may want to navigate away from this page altogether—but since we’re busy hydrating, we’re keeping them on the current page they no longer care about.

How can we solve these problems?

There is one thing in common between these problems. They force you to choose between doing something early (but then hurting UX because it blocks all other work), or doing something late (but hurting UX because you’ve wasted time).

This is because there is a “waterfall”: fetch data (server) → render to HTML (server) → load code (client) → hydrate (client). Neither of the stages can start until the previous stage has finished for the app. This is why it’s inefficient. Our solution is to break the work apart so that we can do each of these stages for a part of the screen instead of entire app.

This is not a novel idea: for example, Marko is one of JavaScript web frameworks that implements a version of this pattern. The challenge was in how to adapt a pattern like this to the React programming model. It took a while to solve. We introduced the <Suspense> component for this purpose in 2018. When we introduced it, we only supported it for lazy-loading code on the client. But the goal was to integrate it with the server rendering and solve these problems.

Let’s see how to use <Suspense> in React 18 to solve these issues.

React 18: Streaming HTML and Selective Hydration

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

To see what these features do and how they solve the above problems, let’s return to our example.

Streaming HTML before all the data is fetched

With today’s SSR, rendering HTML and hydration are “all or nothing”. First you render all HTML:

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section>
    <!-- Comments -->
    <p>First comment</p>
    <p>Second comment</p>
  </section>
</main>

The client eventually receives it:

Then you load all the code and hydrate the entire app:

But React 18 gives you a new possibility. You can wrap a part of the page with <Suspense>.

For example, let’s wrap the comment block and tell React that until it’s ready, React should display the <Spinner /> component:

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

By wrapping <Comments> into <Suspense>, we tell React that it doesn’t need to wait for comments to start streaming the HTML for the rest of the page. Instead, React will send the placeholder (a spinner) instead of the comments:

Comments are nowhere to be found in the initial HTML now:

<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>

The story doesn’t end here. When the data for the comments is ready on the server, React will send additional HTML into the same stream, as well as a minimal inline <script> tag to put that HTML in the “right place”:

<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

As a result, even before React itself loads on the client, the belated HTML for comments will “pop in”:

This solves our first problem. Now you don’t have to fetch all the data before you can show anything. If some part of the screen delays the initial HTML, you don’t have to choose between delaying all HTML or excluding it from HTML. You can just allow that part to “pop in” later in the HTML stream.

Unlike traditional HTML streaming, it doesn’t have to happen in the top-down order. For example, if the sidebar needs some data, you can wrap it in Suspense, and React will emit a placeholder and continue with rendering the post. Then, when the sidebar HTML is ready, React will stream it along with the <script> tag that inserts it in the right place— even though the HTML for the post (which is further in the tree) has already been sent! There is no requirement that data loads in any particular order. You specify where the spinners should appear, and React figures out the rest.

Note: for this to work, your data fetching solution needs to integrate with Suspense. Server Components will integrate with Suspense out of the box, but we will also provide a way for standalone React data fetching libraries to integrate with it.

Hydrating the page before all the code has loaded

We can send the initial HTML earlier, but we still have a problem. Until the JavaScript code for the comments widget loads, we can’t start hydrating our app on the client. If the code size is large, this can take a while.

To avoid large bundles, you would usually use "code splitting": you would specify that a piece of code doesn't need to load synchronously, and your bundler will split it off into a separate <script> tag.

You can use code splitting with React.lazy to split off the comments code from the main bundle:

import { lazy } from 'react';

const Comments = lazy(() => import('./Comments.js'));

// ...

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>

Previously, this did not work with server rendering. (To the best of our knowledge, even popular workarounds forced you to choose between either opting out of SSR for code-split components or hydrating them after all their code loads, somewhat defeating the purpose of code splitting.)

But in React 18, <Suspense> lets you hydrate the app before the comment widget has loaded.

From the user’s perspective, initially they see non-interactive content that streams in as HTML:

Then you tell React to hydrate. The code for comments isn’t there yet, but it’s okay:

This is an example of Selective Hydration. By wrapping Comments in <Suspense>, you told React that they shouldn’t block the rest of the page from streaming—and, as it turns out, from hydrating, too! This means the second problem is solved: you no longer have to wait for all the code to load in order to start hydrating. React can hydrate parts as they’re being loaded.

React will start hydrating the comments section after the code for it has finished loading:

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

Hydrating the page before all the HTML has been streamed

React handles all of this automatically, so you don’t need to worry about things happening in an unexpected order. For example, maybe the HTML takes a while to load even as it’s being streamed:

If the JavaScript code loads earlier than all HTML, React doesn’t have a reason to wait! It will hydrate the rest of the page:

When the HTML for the comments loads, it will appear as non-interactive because JS is not there yet:

Finally, when the JavaScript code for the comments widget loads, the page will become fully interactive:

Interacting with the page before all the components have hydrated

There is one more improvement that happened behind the scenes when we wrapped comments in a <Suspense>. Now their hydration no longer blocks the browser from doing other work.

For example, let’s say the user clicks the sidebar while the comments are being hydrated:

In React 18, hydrating content inside Suspense boundaries happens with tiny gaps in which the browser can handle events. Thanks to this, the click is handled immediately, and the browser doesn’t appear stuck during a long hydration on a low-end device. For example, this lets the user navigate away from the page they’re no longer interested in.

In our example, only comments are wrapped in Suspense, so hydrating the rest of the page happens in a single pass. However, we could fix this by using Suspense in more places! For example, let’s wrap the sidebar as well:

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

Now both of them can be streamed from the server after the initial HTML containing the navbar and the post. But this also has a consequence on hydration. Let’s say the HTML for both of them has loaded, but the code for them has not loaded yet:

Then, the bundle containing the code for both the sidebar and the comments loads. React will attempt to hydrate both of them, starting with the Suspense boundary that it finds earlier in the tree (in this example, it’s the sidebar):

But let’s say the user starts interacting with the comments widget, for which the code is also loaded:

React will synchronously hydrate the comments during the capture phase of the click event:

As a result, comments will be hydrated just in time to handle the click and respond to interaction. Then, now that React has nothing urgent to do, React will hydrate the sidebar:

This solves our third problem. Thanks to Selective Hydration, we don’t have to “hydrate everything in order to interact with anything”. React starts hydrating everything as early as possible, and it prioritizes the most urgent part of the screen based on the user interaction. The benefits of Selective Hydration become more obvious if you consider that as you adopt Suspense throughout your app, the boundaries will become more granular:

In this example, the user clicks the first comment just as the hydration starts. React will prioritize hydrating the content of all parent Suspense boundaries, but will skip over any of the unrelated siblings. This creates an illusion that hydration is instant because components on the interaction path get hydrated first. React will hydrate the rest of the app right after.

In practice, you would likely add Suspense close to the root of your app:

<Layout>
  <NavBar />
  <Suspense fallback={<BigSpinner />}>
    <Suspense fallback={<SidebarGlimmer />}>
      <Sidebar />
    </Suspense>
    <RightPane>
      <Post />
      <Suspense fallback={<CommentsGlimmer />}>
        <Comments />
      </Suspense>
    </RightPane>
  </Suspense>
</Layout>

With this example, the initial HTML could include the <NavBar> content, but the rest would stream in and hydrate in parts as soon the associated code is loaded, prioritizing the parts that the user has interacted with.

Note: You might be wondering how your app can work in this not-fully-hydrated state. There are a few subtle details in the design that make it work. For example, instead of hydrating each individual component separately, hydration happens for entire <Suspense> boundaries. Since <Suspense> is already used for content that doesn't appear right away, your code is resilient to its children not being immediately available. React always hydrates in the parent-first order, so the components always have their props set. React holds off from dispatching events until the entire parent tree from the point of the event is hydrated. Finally, if a parent updates in a way that causes the not-yet-hydrated HTML to become stale, React will hide it and replace it with the fallback you specified until the code has loaded. This ensures the tree appears consistent to the user. You don’t need to think about it, but that’s what makes it work.

Demo

We've prepared a demo you can try to see how the new Suspense SSR Architecture works. It is artifically slowed down, so you can adjust the delays in server/delays.js:

In Conclusion

React 18 offers two major features for SSR:

These features solve three long-standing problems with SSR in React:

The <Suspense> component serves as an opt-in for all of these features. The improvements themselves are automatic inside React and we expect them to work with the majority of existing React code. This demonstrates the power of expressing loading states declaratively. It may not look like a big change from if (isLoading) to <Suspense>, but it's what unlocks all of these improvements.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4