Replies: 9 comments 14 replies
-
This is a superb explanation of a very complex topic. Thank you! |
Beta Was this translation helpful? Give feedback.
-
Really helpful post especially in response to "Is Suspense just a fancy loading spinner?". Isn't event replaying completely new in React 18? Maybe I'm over-valueing it but I think it's not emphasized enough. |
Beta Was this translation helpful? Give feedback.
-
This is an awesome detailed explanation 💯 🎉
How do you make sure that the browser is still interactive during hydration? Is it like doing hydration in batches at some intervals? Let's say we have a component |
Beta Was this translation helpful? Give feedback.
-
This is really exciting! Potentially, I suppose this means that Are you planning to extract changes to React internal caches and serialise those? or provide a different method to provide serialised data on the server-side? Overall, on another note, I'm really hyped about this, since this gives us a path to deprecate and replace |
Beta Was this translation helpful? Give feedback.
-
this is really exciting - thank you for posting such a great explanation! i know data fetching support is still incomplete, but one thing i wanted to confirm was the default behavior for the following use case: <Suspense fallback={<Spinner />}
<SomeComponentThatDependsOnData />
</Suspense> Is my understanding correct that when the client goes to hydrate, it will also fetch the data dependency and trigger the Suspense boundary, but instead of showing the |
Beta Was this translation helpful? Give feedback.
-
What about SEO / Status Codes?I got an interesting question from Twitter:
I want to reproduce it here since it came up a few times in other conversations. |
Beta Was this translation helpful? Give feedback.
-
The general principle of hydration here is that you can think of it as lazy. In that something only needs to hydrate at all if it's being interacted with. However hydration is also used for other things like starting subscriptions to changes, auto playing video with more control, shifting an hscroll on a timer etc. It's also worth hydrating just to have it warmed up so it response quicker. That's why we also eagerly hydrate things at the lowest priority in the background too. However sometimes some things are more important to hydrate early than others. There's an API (which might change) called You can use this to do user space features like hydrate the content in viewport faster than other content, or hydrating that node that you know contains auto-playing video. |
Beta Was this translation helpful? Give feedback.
-
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.
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:
renderToString
to the newrenderToPipeableStream
method, as described here.hydrateRoot
on the client and then start wrapping parts of your app with<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:
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: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:
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”: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: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:
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:
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.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
:API_DELAY
lets you make the comments take longer to fetch on the server, showcasing how the rest of the HTML can be sent early.JS_BUNDLE_DELAY
lets you delay the<script>
tags from loading, showcasing how the comment widget’s HTML "pops in" even before React and your application bundle have been downloaded.ABORT_DELAY
lets you see the server "giving up" and handing off rendering to the client if fetching on the server takes too long.In Conclusion
React 18 offers two major features for SSR:
<script>
tags that put them in the right places.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 fromif (isLoading)
to<Suspense>
, but it's what unlocks all of these improvements.Beta Was this translation helpful? Give feedback.
All reactions