React Server Component is a new feature of React that allows the server and the client(browser) to collaborate in rendering React applications. Simply put, it moves part of the rendering process to the server to improve the overall performance.
To better understand why we need to move the rendering process to the server, let’s look at how the client-side render method renders a react element tree(JSX):
1 | // App Component |
The above Components will be transformed into Virtual DOM
(react element tree) after we use React.createElement
to render it:
1 | const App = { |
Then ReactDOM.render
will render this react element tree into HTML
format and attach it to HTML.
Two things to notice here are:
ComponentB
‘s fetchData
, it will slow down the tree construction and occupy the resource of the browser.RSC improves the performance via separating those calculation-intense components rendered as server components, while those components that create interactive logic are handled by the client as client components.
When a client requires a server component, the server side will first construct the react element tree by parsing all server components on the tree skipping all client components, and leaving a placeholder with an instruction to tell the client side to fill it up in a browser(We will walk through the detail implementation later).
So when user open a page build with rsc, they will typically see server components are rendered, and then the components with interactive logic are rendered.
There is another concept that is easy to mix with RSC, which is SSR(server-side rendering). Though they all doing some jobs in server side, they are quite different.
The main difference is granularity. SSR renders a page on server side each request, while the RSC render a component each request, and it request the client side to fill up all the placeholder inside the component to finish.
In other word, SSR’s output is a HTML, and RSC’s output is a react element tree.
In SSR, after client side gets the HTML, it will use (hydrate(reactNode, domNode, callback?)[https://react.dev/reference/react-dom/hydrate] to attach the html with react node, so that the react can take care of DOM(enabling interactive events). And RSC doesn’t require hydrate process because client components are rendered in browser.
So what is the benefits of RSC? The browser is a good place for this, because it allows your React application to be interactive — you can install event handlers, keep track of state, mutate your React tree in response to events, and update the DOM efficiently. But in these use cases, react server components could be a better choice:
Without further ado, let’s start introducing how react(and other frameworks) implement RSC. To control the complexity, we will separate the RSC system into several components, simplified them and build them one by one.
The life of a page using RSC always starts at the server, server will render an incomplete serializable react element tree and send to client, let the client to fill up the tree and render as HTML. Let’s start by looking at how server render an serializable react element tree in this section.
.server.jsx
and .client.jsx
. This Definition is easy for humans and bundlers to tell them apart.We modify our previous example a bit using RSC like this:
1 | // App.server.jsx |
The serialization output is like this:
1 | const App = { |
There are two things that it is not available to serialize
here:
type
field: type
field is a function when it is a react component, which is not JSON-serializable.(It is coupling with memory.)onClick: () => alert("cool!")
function, which is also not JSON-serializable.To JSON-stringify everything, react passes a replacer
function to JSON.stringify
, to deal with the type
function reference and replace client component with a placeholder. Check out the source code (resolveModelToJSON)[https://github.com/facebook/react/blob/42c30e8b122841d7fe72e28e36848a6de1363b0c/packages/react-server/src/ReactFlightServer.js#L368].
Here I create a simplified version to help you understand:
1 | // value is the react model we pass |
The output of client component is like this:
1 | { |
This whole process happens during bundling. React has a official react-server-dom-webpack loader which can match ***.client.jsx** and turn them to moduleReference
object.
At the end of the serialization process, we send a JSON that represent this serializable React tree to client like this:
Three things happened:
moduleReference
object.Now we managed to separate part of work to server, so that we can expect a better performance when rendering. Because the output is serializable, we can do even more. Now client side needs to wait until the whole react element tree is received from server side, there are a lot of time is wasted in client side. We can apply streaming concept to make this process progressively. Streaming means that server side will send the react element tree slice by slice during the construction process, instead of sending the whole tree until the construction is done. The Suspense feature in react plays an important role to implement this.
1 | // The Suspense component allows us to wait for the DataFetchingComponent to load |
There are two places that RSC will use suspense feature to optimaze performance:
Here is the simple implementation:
1 | export function resolveModelToJSON( |
Remember when server sends react element tree to client side, client components are still yet to render, so they will suspense first until the client renders them.
This suspense and stream feature enable the browser too incrementally render the data as they become available.
You might wonder how can a react element tree(JSON) become a stream, react server component use a simple format to make this possible. The format is like this:
1 | M1:{"id":"./src/ClientComponent.client.js","chunks":["client1"],"name":""} |
The lines that start with M
defines a client component module reference, with the information needed to look up the component function in the client bundles.
The line starting with J
defines an actual React element tree, with things like @1
referencing client components defined by the M
lines.
This format is very streamable — Given that each row is a module/component, as soon as the client has read a whole row, it can parse a snippet of JSON and make some progress.
1 | // Tweets.server.js |
The output stream is like this:
1 | M1:{"id":"./src/ClientComponent.client.js","chunks":["client1"],"name":""} |
J0
now has a suspense boundary, and inside the boundary, there is a @3
that has not finished rendering and pushed to the stream. RSC continue push M4
to the stream and when @3
is ready, it is push to the stream as J3
.
When client receives the React Tree stream, it will start reconstructing the completed React tree and render to HTML progressively.
React provides a method called createFromFetch
in react-server-dom-webpack
to render RSC. The basic usage is like this:
1 | import { createFromFetch } from 'react-server-dom-webpack' |
response.readRoot()
read the stream from /rsc?...
and update the react element progressively.
Frontend, React, Software Engineering — Apr 10, 2024
Made with ❤ and at Earth.