- New Suspense SSR Architecture in React 18
- Original author:
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: NieZhuZhu
- Proofread by: Kimberly and Zavier
An overview of the
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 selective mechanisms you need to be aware of, especially if you don’t use frameworks.
The main new API is pipeToNodeWritable, which you can see in Upgrading to React 18 on the Server. We plan to do more implementation in detail, as this is not the final version and there are still some things that need to be worked out.
The main API out there is
.
This article is a brief overview of the new architecture, its design and the problems it solves.
In a nutshell
Server-side rendering (abbreviated to “SSR” in this article) allows you to generate HTML from the React component on the server and send that HTML to your users. SSR allows your users to see the content of a page before your JavaScript package loads and runs.
In React, SSR is always performed in several steps:
- Get data for the entire application on the server.
- The entire application is then rendered into HTML on the server and returned in response.
- The JavaScript code for the entire application is then loaded on the client side.
- Then, on the client side, the JavaScript logic is bound to the HTML generated by the server for the entire application (a process called ‘Encounter’).
The key is that each step must complete the entire application at once before the next step can begin. This is not efficient if some parts of your application are slower than others. This is a problem for almost every app of any size.
React 18 lets you use Suspense> to break your application up into smaller, independent units. These units do these steps independently and do not block the rest of the application. As a result, users of your application will see the content faster and can start interacting with the application faster. The slowest parts of your application don’t drag down the faster parts. These optimizations are automatic. You don’t need to write any special code to do this.
This also means that React.lazy can now “work” with SSR. Here’s a demo.
If you don’t use frames, you’ll need to change the way HTML is generatedwired up).
What is SSR?
When the user loads your application, you want to display a fully interactive page as soon as possible:
This illustration uses green to represent the interactive parts of the page. In other words, all of their JavaScript event handlers are already bound, buttons can be clicked to update status, and so on.
However, the page is not interactive until its JavaScript code is fully loaded. This includes React itself and your application code. For applications of a certain size, most of the load time will be spent downloading your application code.
If you don’t use SSR, the only thing the user sees when the JavaScript loads is a blank page.
That’s not very good, that’s why we recommend using SSR. SSR lets you render your React component into HTML on the server and send it to the user. HTML is not very interactive (except for simple built-in web interactions such as links and form entry). However, it gives the user something to see while the JavaScript is still loading.
Here, the gray parts of the screen represent parts that are not yet fully interactive. The JavaScript code for your application has not yet loaded, so clicking the button will not get you any response. But especially for sites with lots of content, SSR is useful because it allows users with poor Internet connections to start reading or viewing content while JavaScript loads.
When React and your application code are both loading, you want the HTML to be interactive. You tell React: “This is the App component that generates this HTML on the server. Bind event handlers to that HTML!” React renders your component tree in memory, but instead of generating DOM nodes for it, it binds all the logic to existing HTML.
This process of rendering components and binding event handlers is called ‘Encounter’. This is like using event handlers as “water” to irrigate “dry” HTML. At least, that’s how I explain the term to myself.)
After chocolate, ‘React works’ : Your components can set states, respond to clicks, and so on:
You can see that SSR is kind of like “magic”. It won’t make your application fully interactive any faster. Instead, it lets you display the non-interactive version of your application faster so users can view static content while waiting for JS to load. However, this makes a big difference for people with poor Internet connections and improves overall perceptual performance. It also helps your search engine rankings, both because of easier indexing and faster response times.
Note: Do not confuse SSR with server components. The server component is a more experimental feature that is still being researched and may not be part of the initial React 18 release. Are you fromhereYou can learn about server components. Server components are complementary to SSR and will be one of the recommended ways to get data, but they are not covered in this article.
What are the problems with SSR today?
The above approach is feasible, but in many ways it is not optimal.
Everything must be acquired before anything can be shown
One problem with SSR today is that it doesn’t allow components to “wait for data.” In the current API, when you render to HTML, you must already have all the data ready for your component on the server. This means you have to collect all the data on the server before you can start sending any HTML to the client. It’s inefficient.
For example, suppose you want to render a post with a comment. It’s important to display comments as early as possible, so include them in your server’s HTML output. But your database or API layer is slow, and you have no control over that. Now, you have to make some tough choices. If you exclude them from the server output, the user won’t see them until the JS is loaded. But if you include them in the server output, you’ll have to delay sending the rest of the HTML (for example, the navigation bar, the sidebar, or even the article content) until the comments are loaded and you can render the full component tree. That’s not good.
Incidentally, some data retrieval schemes repeatedly attempt to render the tree into HTML and discard the results until the data is resolved. Because React doesn’t offer more ergonomic options. We want to offer a solution that doesn’t require such extreme compromise.
You have to pack everything before you can tell anything about chocolate
After your JavaScript code loads, you’ll tell React to add HTML “hydrate” and make it interactive. React will “walk” through the HTML generated by the server when rendering your components and bind event handlers to that HTML. For this to work, the tree your component generates in the browser must match the tree generated by the server. Otherwise React won’t “match them!” One very unfortunate consequence of this is that you have to load JavaScript for all components on the client side before you can begin to encounter any of them
For example, suppose the comment widget contains a lot of complex interaction logic and takes some time to load JavaScript for it. Now you have to make the hard choices again. It is a good idea to render the comments on the server into HTML so that they can be displayed to the user as soon as possible. However, since Year-to-date Hydration is only completed once, you can’t start the Hydrate navigation bar, sidebar, and article content without loading the code for the comment widget. Of course, you can use code splitting and loading separately, but you must exclude comments from the server HTML. Otherwise React won’t know what to do with this HTML (where’s the code?). “And deleted it during his encounter with Chocolate.
You must hydrate everything before you can interact with anything
Chocolate suffers from a similar problem. Today, React finishes the tree in one go. This means that once it starts hydrate (essentially calling your component functions), React does not stop the hydration process until it completes hydration for the entire tree. Therefore, you must wait for all components to be thirsty before you can interact with any components.
For example, let’s say the comment widget has expensive rendering logic. It may run fast on your computer, but it’s not cheap to run the logic on low-end devices and can lock the screen for several seconds. Ideally, of course, we wouldn’t have this logic on the client side (this is something the server component can help solve). But for some logic, it’s inevitable. This is because it determines what the attached event handler should do and is critical to interactivity. Thus, once told, users cannot interact with the navigation bar, sidebar, or post content until the entire tree completes hydration. This is particularly unfortunate for navigation, as users may want to leave the page entirely, but since we are busy with chocolate, we keep them on the current page that they no longer care about.
How can we solve these problems?
There is a common thread between these problems. They force you to choose between doing something early (but the user experience is compromised because it blocks all other work), or doing something late (but the user experience is compromised because you’re wasting time).
This is because of a “waterfall” (process) : fetching data (server) → rendering to HTML (server) → loading code (client) → hydration (client). No phase should begin before the end of the previous one. That’s why it’s inefficient. Our solution was to split up the work so that we could do these stages for parts of the screen rather than the entire application.
This is not a novel idea: For example, Marko is a JavaScript web framework that implements this pattern. Adapting this pattern to the React programming model has been challenging. So it took us a while to figure this out. We introduced Suspense> components for this purpose in 2018. When we introduced it, we only supported lazy loading code on the client side. But our goal is to combine it with server rendering to solve these problems.
Let’s take a look at how to use Suspense> in React 18 to solve these problems.
React 18: Streaming HTML and Selective hydration
In React 18, there are two main SSR features unlocked by Suspense.
- Stream HTML on the server. To use this feature, you need to start from
renderToString
Switch to newpipeToNodeWritable
Methods, such asDescribed here. - Pick up on the client side. To use this feature, you need to be on the client sideSwitch to the
createRoot
“And start using it<Suspense>
Wrap part of your application.
To see what these features do and how they solve the above problems, let’s go back to our example.
Use streaming HTML until all the data has been retrieved
In today’s SSR, rendering HTML and hydration are “all or nothing”. First, you render all the 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>Copy the code
The client will eventually receive it:
Then you load all the code and change the entire app:
But React 18 gives you a new possibility. You can use Suspense to wrap parts of your page.
For example, let’s wrap the comment block and tell React that React should display the
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
Copy the code
By wrapping
as
, we tell React that it can start transferring HTML for the rest of the page without waiting for Comments. Instead, React will send placeholders (a spreader) instead of comments:
The comments are now not found in the original 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 id="comments-spinner"> <! -- Spinner --> <img width=400 src="spinner.gif" alt="Loading..." /> </section> </main>Copy the code
This is not the end of the matter. When the comment data is ready on the server, React sends additional HTML to the same stream, along with a minimal inline
<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>Copy the code
As a result, the HTML for the late comment will “pop up” even before React itself is loaded into the client.
That solves our first problem. Now you don’t have to get all the data before you show anything. If some part of the screen delays the original HTML, you don’t have to choose between delaying all HTML or excluding it from HTML. You can only allow that part of the content to “flood in” later in the HTML stream.
Unlike traditional streaming HTML, it doesn’t have to happen in a top-down order. For example, if the sidebar needs some data, you can wrap it in Suspense and React will issue a placeholder and continue rendering the post. Then, when the HTML for the sidebar is ready, React will flow it out with the
Note: For this to work, your data capture solution needs to integrate with Suspense. The server components will work with Suspense out of the box, but we will also provide a way to integrate with standalone React data fetching libraries.
Tells the page before all the code loads
We can send the original HTML ahead of time, but we still have a problem. We couldn’t start our application on the client side without loading the JavaScript code for the comment widget. If the size of the code is large, this may take a while.
To avoid large packages, you often use “code splitting” : you can specify that a piece of code does not need to be loaded synchronously and that your packaging tool will split it into a separate
You can use react.lazy for code splitting to separate comment code from the main package.
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
Copy the code
Previously, this did not work with server rendering. (As far as we know, even popular workarounds force you to choose between choosing not to use THE SSR of a code-splitting component, or to encounter all of it after loading, which somehow defeats the purpose of code-splitting.)
But in React 18,
lets you start the Hydrate app before the comment widget loads.
From the user’s perspective, initially what they see is non-interactive content streaming in as HTML.
Then you tell React to encounter with chocolate. The code for the comment isn’t there yet, but that’s okay:
This is an example of selective hydration. By wrapping Comments in Suspense, you tell React that they shouldn’t stop the rest of the page from streaming — and, as it turns out, they shouldn’t stop hydration either. This means the second problem is solved: you no longer have to wait for all the code to load before you begin to reveal your identity. React can be loaded while performing transformations.
React begins its transformation of the comments section after the code is loaded:
One heavy JS doesn’t prevent the rest of the page from being interactive, thanks to some variations.
First, before all the HTML is fluidized, take a hiatus from the page
React handles all of this automatically, so you don’t need to worry about things happening in unexpected order. For example, maybe HTML takes a while to load, even though it’s being streamed:
If JavaScript code loads before all HTML, React has no reason to wait! Tells of his encounter for the rest of the page:
When the HTML for the comment loads, it will appear as non-interactive because JS has not yet appeared:
Finally, when the JavaScript code for the comment widget loads, the page becomes fully interactive:
Interact with pages before all components complete Hydration
There is another improvement that happens behind the scenes when we wrap comments in Suspense. Now their embrace doesn’t stop browsers from doing other things.
For example, suppose a user clicks on the sidebar when commenting on his encounter with Chocolate:
In React 18, the browser can handle events in Suspense during variations in Suspense. Thanks to this, clicks are processed immediately, and browsers don’t lag after a long period of hydration on low-end devices. For example, this allows users to navigate away from a page they are no longer interested in.
In our example, only the comments are wrapped in Suspense, so the encounter with the rest of the page is one-off. However, we can solve this problem by using Suspense in more places. For example, let’s wrap up the sidebar as well.
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
Copy the code
Both can now be circulated from the server after initial HTML containing a navigation bar and a post. But it also affects hydration. For example, their HTML is loaded, but their code is not yet loaded:
The package containing the sidebar and comment code is then loaded. React will try to take a bite out of them, starting with the Suspense boundary it finds earlier in the tree (in this case, the sidebar) :
However, suppose the user starts interacting with the comment widget and its code is loaded:
React logs clicks and gives preference to comments because it is more urgent:
After commenting and being thirsty, React “replay” the recorded click events (by dispatching them again) and have your components React to the interaction. Then, there is nothing urgent about React right now, so React gives the sidebar a makeover:
That solves our third problem. Thanks to selective hydration, we don’t have to “embrace everything in order to interact with everything”. React begins his transformation of everything early. It prioritizes the most urgent parts of the screen based on the user’s interactions. The benefits of selective hydration are even more apparent when you consider that Suspense becomes more granular throughout your application:
In this case, users hit the first comment at the beginning of their encounter with chocolate. React gives preference to all content with the Suspense boundary of fathers, but skips any unrelated siblings. Because components on the interaction path are preferred to be dehydrated, this creates the illusion that hydration is immediate. React takes a bite out of the rest of the app later.
In practice, you might add Suspense around the root of your application:
<Layout>
<NavBar />
<Suspense fallback={<BigSpinner />}>
<Suspense fallback={<SidebarGlimmer />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<CommentsGlimmer />}>
<Comments />
</Suspense>
</RightPane>
</Suspense>
</Layout>
Copy the code
In this example, the initial HTML could include the
Note: You may be wondering how your app can function even if it’s not quite thirsty. There are subtle details in the design that make it work. For example, variations are revealed not for each individual component individually, but for the whole
<Suspense>
Tells of his first encounter with chocolate. because<Suspense>
Has been used for things that don’t appear immediately, so your code is adaptive to situations where its children don’t appear immediately. React always performs transformations in parent-first order, so components always have their props combinations. React does not assign events before the entire parent tree where the event takes place. Finally, if the update method of the parent class causes the HTML that is not thirsty to become stale, React will hide it and use the one you specifyfallback
To replace it until the code is loaded. This ensures that the tree appears consistent to the user. You don’t need to think about that, but that’s where the feature comes in.
Demo
We’ve prepared a demo you can try to see how the new Suspense SSR architecture works. It has been artificially slowed down so you can adjust the delays in server/delays.
API_DELAY
Lets you make comments take longer to fetch on the server, showing how the rest of the HTML is sent ahead of time.JS_BUNDLE_DELAY
Let you delay<script>
The load TAB shows how the HTML for the comment widget “pops up” before React and your app package is downloaded.ABORT_DELAY
Lets you see the server “give up” and hand over the rendering to the client if the server takes too long to fetch.
conclusion
React 18 provides two main features for SSR:
- Streaming HTML lets you start sending HTML as early as possible, with the additional content of streaming HTML
<script>
Put labels together in the right place. - Selective Hydration allows you to start implementing chocolate for your application early, before the HTML and JavaScript code is fully downloaded. It also prioritizes the portion of the user who is interacting with chocolate, creating an illusion of instant hydration.
These features address three long-standing issues with SSR in React:
- You no longer need to wait for all the data to load on the server before sending HTML. Instead, once you have enough data to display the shell of your application, you start sending the HTML, and stream the rest of the HTML when it’s ready.
- You no longer have to wait for all the JavaScript to load to begin with chocolate. Instead, you can use code splitting and server rendering. The server HTML will be retained, and React will update the related code when it loads.
- You no longer need to wait for all components to be thirsty before you start interacting with the page. Instead, you can rely on selective hydration, which prioritizes the components that users are interacting with and reveals them early on.
Suspense components serve as a choice for all of these features. These improvements are themselves automated within React, and we expect them to work with most of the existing React code. This demonstrates the power of declaratively expressing load state. Going from if (isLoading) to Suspense> might not look like a big change, but it’s the key to unlocking these improvements.
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.