Overview
React 18 will include architectural improvements to React server-side rendering (SSR) performance. These improvements are substantial and are the result of years of work. These improvements are mostly carried out behind the scenes, but there are some selective mechanism you need to pay attention, especially if you not use framework words.
The main new API is pipeToNodeWritable
, you can Upgrading to React 18 on the Server . We plan to do more implementation in details, because this is not the final version, and there are still some things that need to be resolved.
The main existing API is <Suspense>
.
This article is a brief overview of the new architecture, its design and the problems it solves.
in short
Server-side rendering (abbreviated as "SSR" in this article) allows you to generate HTML from React components on the server and send the HTML to your users. SSR allows your users to see the content of the page before your JavaScript package loads and runs.
SSR in React is always done in several steps:
- Get the data of the entire application on the server.
- Then render the entire application into HTML on the server and return it in the response.
- Then load the JavaScript code of the entire application on the client side.
- Then bind the JavaScript logic to the HTML generated by the server for the entire application on the client side (this process is called "hydration").
The key is that each step must complete the work of the entire application at once before the next step. If some parts of your application are slower than others, this is not efficient. This is also a problem faced by almost all applications of a certain scale.
React 18 lets you use <Suspense>
to break your application into smaller independent units. These units will complete these steps independently and will not hinder other parts of the application. As a result, users of your application will see the content faster and can start interacting with the application sooner. The slowest parts of your application will not drag down the faster parts. These optimizations are automatic. You don't need to write any special code to achieve this function.
This also means that React.lazy
can now "work normally" with SSR. Here is a demo .
(If you don't use the framework, you will need to change the specific way of HTML generation wired 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 express the interactive parts of the page. In other words, all of their JavaScript event handlers are already bound, click the button to update the state and so on.
However, the page cannot be interactive until the JavaScript code of the page is fully loaded. This includes React itself and your application code. For applications of a certain scale, most of the load time will be used to download your application code.
If you don't use SSR, the only thing users see when JavaScript loads is a blank page.
This is not very good, which is why we recommend using SSR. SSR allows you to your React components into HTML on the 16119a386b817b server and send it to the user. HTML is not very interactive (except for simple built-in network interactions, such as link and form input). However, it allows the user to see something while the JavaScript is still loading.
Here, the gray part of the screen represents the part that is not yet fully interactive. The JavaScript code of your application has not been loaded yet, so there is no response when you click the button. But especially for websites with complex content, SSR is very useful because it allows users with poor network connections to start reading or viewing content when JavaScript is loaded.
When both React and your application code are loading, you need to make this HTML interactive. You tell React: "This is the App
component that generates this HTML on the server. Bind the event handler to the HTML!" React will render your component tree in memory, but instead of generating DOM nodes for it, Bind all logic to existing HTML.
This process of rendering components and binding event handlers is called "hydration". (It's like using event handlers as "water" to water "dry" HTML. At least, this is how I explained the term to myself.)
After hydration, it is "React normal operation": your component can set state, respond to clicks, etc.:
You can see that SSR is a bit like "magic". It cannot make your application fully interactive faster. Instead, it allows you to display a non-interactive version of your application faster so that users can view static content while waiting for the JS to load. However, this trick makes a big difference for people with poor network connections and improves overall perception performance. It also helps your search engine rankings, both because of easier indexing, and because of faster response times.
Note: Do not confuse SSR with server components. The server component is a more experimental feature that is still under research and may not be part of the initial version of React 18. You can learn about server components Server components are a supplement to SSR and will become one of the recommended methods of data acquisition, but this article does not introduce them.
What are the problems with SSR today?
The above method is feasible, but in many ways, it is not optimal.
Before showing anything, you must get everything
One problem with SSR today is that it does not allow components to "wait for data." In the current API, when you render to HTML, you must have prepared all the data for your component on the server. This means that you must collect all on the server before you can start sending any HTML to the client. This is very inefficient.
For example, suppose you want to render a post with comments. It is important to display comments as early as possible, so you need to include them in the server's HTML output. But your database or API layer is very slow, which is beyond your control. Now, you have to make some difficult choices. If you exclude them from the server output, users will not see them until the JS is loaded. But if you include them in the server output, you must postpone sending the rest of the HTML (for example, the navigation bar, sidebar, and even the content of the article) until the comments are loaded before you can render the complete component tree. This is not good.
By the way, some data acquisition schemes will repeatedly try to render the tree into HTML and discard the result until the data is resolved. Because React does not provide more ergonomic options. We want to provide a solution that does not require such extreme compromises.
You must install everything before you can hydration anything
After your JavaScript code loads, you tell React to "hydrate" the HTML and make it interactive. React will "walk" through the HTML generated by the server when rendering your component and bind event handlers to the HTML. In order for it to work, the tree generated by your component in the browser must match the tree generated by the server. Otherwise React can’t "match them!" A very unfortunate consequence of this is that you have to load the JavaScript of all on the client to start hydration of any
For example, suppose that the comment widget contains a lot of complex interaction logic, and it takes some time to load JavaScript for it. Now you have to make difficult choices again. It is a good way to render comments on the server into HTML so that they can be displayed to users as soon as possible. However, because today's hydration can only be done once, you can't start to hydrate the navigation bar, sidebar, and article content before loading the code of the comment widget. Of course, you can use code splitting and load separately, but you must exclude comments from the server HTML. Otherwise, React will not know how to deal with this piece of HTML (where is its code?) and will delete it during the hydration process.
Before interacting with anything, you must hydrate everything
hydration itself has a similar problem. Now, React completes the hydration of the tree at once. This means that once it starts hydrate (essentially calling your component function), React will not stop the hydration process until it completes the hydration for the entire tree. So, you have to wait all components are hydrated, talent and any interacting components.
For example, we said that the comment widget has expensive rendering logic. It may run very fast on your computer, but the cost of running these logic on low-end devices is not low, and may even lock the screen for several seconds. Of course, under ideal circumstances, we would not have such logic on the client side (this is a problem that the server component can help solve). But for some logic, this is inevitable. This is because it determines what the attached event handler should do, and it is essential for interactivity. Therefore, once the hydration is started, the user cannot interact with the navigation bar, sidebar, or article content until the entire tree has completed the hydration. This is particularly unfortunate for navigation, because users may want to leave this page completely, but since we are busy with hydration, we leave them on the current page that they no longer care about.
How do we solve these problems?
There is one thing in common between these issues. They force you to choose between doing something early (but because it hinders all other work and causing the user experience to be compromised), or doing something late (but because you waste time, causing the user experience to be compromised).
This is because there is a "waterfall" (process): get data (server) → render into HTML (server) → load code (client) → hydration (client). No stage can start before the end of the previous stage. This is why it is very inefficient. Our solution is to separate the work so that we can do these stages of work for a part of the screen instead of the entire application.
This is not a novel idea: for example: Marko is a JavaScript web framework that implements this mode. Adapting this model to the React programming model is challenging. It took us a while to solve this problem. <Suspense>
component for this purpose in 2018. When we introduced it, we only supported it for lazy loading of code on the client side. But our goal is to combine it with server rendering to solve these problems.
Let's see 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 that are unlocked by Suspense.
- Stream HTML on the server. To use this feature, you need to
renderToString
to the newpipeToNodeWritable
method, such as described here . - Selective hydration on the client. To use this feature, you need a client switch to
createRoot
, then began<Suspense>
packaging part of your application.
In order to understand what these functions do and how they solve the above problems, let us return to our example.
Before all the data is captured, use streaming HTML
In today's SSR, rendering HTML and hydration is "all or nothing". First, you have to 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>
The client will eventually receive it:
Then you load all the code and hydration the entire application:
But React 18 gives you a new possibility. You can use <Suspense>
to wrap part of the page.
For example, let's wrap the comment block and tell React that before it is ready, React should display the <Spinner />
component.
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
By packaging <Comments>
<Suspense>
, we tell React that it can start transmitting HTML for other parts of the page without waiting for comments. Instead, React will send a placeholder (a spinner) instead of a comment:
No comments can be found in the original 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 matter is not over here. When the comment data on the server is ready, React will send additional HTML to the same stream, as well as a minimal inline <script>
tag, putting the 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>
Therefore, even before React itself is loaded on the client, the HTML for late comments will "pop up."
This solves our first problem. Now you don't have to get all the data before displaying anything. If some portion of the screen delays the initial HTML, you do not have to delay all choose HTML or exclude between the outside of HTML. You can only allow that part of the content to "flood" later in the HTML stream.
Unlike traditional streaming HTML, it does not necessarily have to happen in a top-down order. For example, if sidebar needs some data, you can wrap it with Suspense, React will send out a placeholder, and then continue to render the post. Then, when the HTML for the sidebar is ready, React will <script>
tag and insert it in the correct position — even though the HTML of the post (further in the tree) has been sent NS! There is no requirement that the data be loaded in any particular order. You specify where the spinner should appear, and React takes care of the rest.
Note: In order for it to work, your data acquisition solution needs to be integrated with Suspense. The server component will work with Suspense out of the box, but we will also provide a way to integrate with the standalone React data acquisition library.
Hydration the page before all the code is loaded
We can send the initial HTML in advance, but we still have a problem. Before loading the JavaScript code of the comment widget, we cannot start hydration of our application on the client side. If the size of the code is large, this may take a while.
To avoid large packages, you usually use "code splitting": you can specify that a piece of code does not need to be loaded synchronously, and your packaging tool will split it into a single <script>
tag.
You can use React.lazy
for code segmentation to separate the comment code from the main package.
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
Previously, this didn't work with server rendering. (As far as we know, even the popular workarounds force you to choose between not using the SSR of the code-splitting component or hydration all the code after it is loaded, which to some extent violates the code The purpose of the split).
But in React 18, <Suspense>
allows you to hydrate the application before the comment widget is loaded.
From the user's point of view, what they initially see is non-interactive content streaming in as HTML.
Then you tell React to hydration. Although the commented code hasn't appeared yet, it doesn't matter:
This is an example of selective hydration. By wrapping Comments
<Suspense>
, you tell React that they shouldn't block other parts of the page from streaming - and, as it turns out, they shouldn't block hydration either. This means that the second problem has been solved: you no longer need to wait for all the code to load before you can start hydration. React can perform hydration at the same time as part of it is loaded.
React will begin to hydration some of the code in the comment section after it is loaded:
Thanks to selective hydration, a heavy piece of JS does not prevent other parts of the page from being interactive.
Before all HTML is streamed, hydration the page
React handles all this automatically, so you don't need to worry about things happening in an unexpected order. For example, maybe HTML takes a while to load, even if it is being streamed:
If the loading time of JavaScript code is earlier than all HTML, React has no reason to wait! It will hydration other parts of the page:
When the comment HTML is loaded, because the JS has not yet appeared, it will appear non-interactive:
Finally, when the JavaScript code of the comment widget loads, the page becomes fully interactive:
Interact with the page before all components complete hydration
When we wrapped the review in <Suspense>
, another improvement happened behind the scenes. Now their hydration no longer hinders the browser from doing other work.
For example, suppose the user clicks on the sidebar while commenting on hydration:
In React 18, the browser can handle events in the tiny gaps that appear during the hydration of the content in Suspense. Thanks to this, the click is processed immediately, and the browser will not freeze during the long hydration process on low-end devices. For example, this can allow users to navigate away from pages they are no longer interested in.
In our example, only comments are wrapped in Suspense, so hydration to other parts of the page is a one-off. However, we can solve this problem 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 after are included in the initial HTML navigation bar and posts spread from the server. But this will also have an impact on hydration. For example, their HTML has been loaded, but their code has not been loaded:
Then, the package containing the sidebar and comment code is loaded. React will try to hydration them, starting from the Suspense boundary it found earlier in the tree (in this example, it's the sidebar):
However, suppose the user starts interacting with the comment widget, and its code is also loaded:
React will record click, and priority hydration to comment, because it is more urgent:
After the comment is hydrated, React "replays" the recorded click event (by dispatching it again) and lets your component react to the interaction. Then, now React has nothing urgent to do, so React will hydration the sidebar:
This solves our third problem. Thanks to selective hydration, we don’t have to “hydration everything in order to interact with anything”. React starts hydration of everything as early as possible. It prioritizes the most urgent part of the screen based on user interaction. If you consider the use of Suspense throughout the application, the boundaries will become more refined, so the benefits of selective hydration will be more obvious:
In this example, when the user clicks on the first comment, it happens to be the beginning of the hydration. React will hydration the content of all parent Suspense boundaries first, but will skip any unrelated sibling nodes. Because components on the interaction path are hydrated first, this creates the illusion that hydration is instantaneous. React will hydration other parts of the application later.
In practice, you might add Suspense near 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>
In this example, the original HTML can include <NavBar>
, but the rest of the content will flow in immediately after the relevant code is loaded, and will be hydrationed in parts, giving priority to the part that the user has interacted with.
Note: You may want to know how your application can operate in this incompletely hydrated state. There are some subtle details in the design that make it work.<Suspense>
each individual component separately, the entire 06119a386b89e2 boundary is hydration. Because<Suspense>
has been used for content that does not appear immediately, your code is adaptive to situations where its children cannot appear immediately. React always hydration in the order of parent first, so components always have their props combination. React does not dispatch events until the entire parent tree hydration where the event occurs. Finally, if the update method of the parent class causes thefallback
HTML to become stale, React will hide it and replace it with the 06119a386b89e5 you specify until the code is loaded. This ensures that the tree appears consistent in front of the user. You don't need to think about this, but that is why this feature works.
Demo
We have prepared a that you can try to see how the new Suspense SSR architecture works. It is slowed down artificially, so you can adjust the delay server/delays.js
API_DELAY
allows you to make comments on the server take longer to fetch, showing how other parts of the HTML can be sent in advance.JS_BUNDLE_DELAY
lets you delay<script>
tag, showing how the HTML of the comment widget "pops up" before React and your app package download.ABORT_DELAY
lets you see the server "give up" and hand over the rendering work to the client when the acquisition time on the server is too long.
Summarize
React 18 provides two main functions for SSR:
- Streaming HTML allows you to start sending HTML as soon as possible. The extra content of Streaming HTML is put in the right place along
<script>
- Selective hydration allows you to start hydration of your application as early as possible before the HTML and JavaScript code is completely downloaded. It also prioritizes hydration for the part the user is interacting with, creating the illusion of instant hydration.
These features solve the three long-standing problems of 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 the application, you start sending HTML, and the rest of the HTML is ready to be streamed.
- You no longer need to wait for all JavaScript to load to start hydration. Instead, you can use code splitting and server rendering. The server HTML will be preserved, and React will hydration the relevant code when it is loaded.
- You no longer need to wait for all the components to be hydrated before you start interacting with the page. Instead, you can rely on selective hydration to prioritize the components the user is interacting with and hydration them as early as possible.
Suspense
component serves as a choice for all these functions. These improvements themselves are made automatically within React, and we expect them to work with most existing React code. This demonstrates the power of declaratively expressing the loading state. From if (isLoading)
to <Suspense>
may not see a big change, but it is the key to unlocking these improvements.
- Original address: New Suspense SSR Architecture in React 18
- Original author: []()
- Translation from: Nuggets Translation Project
- Permanent link to this article: https://github.com/xitu/gold-miner/blob/master/article/2021/new-suspense-ssr-architecture-in-react-18.md
- Translator: NieZhuZhu (playing iron eggs classmates)
- Proofreader: Kimberly , Zavier
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。