All posts

The future of rendering in React

Updated 1 Oct 2023 21 min read

The popularity of React as a UI building library has only been growing and rather accelerating over the past few years. At the time of writing this article, it has 14 million+ weekly npm downloads, which I know isn’t a correct measure for the popularity of a library, but the React Devtools chrome extension alone also has more than 3 million weekly active users. However, the rendering patterns in React have almost been the same until React 18.

So in this article, we’ll be looking at React’s current rendering patterns, their problems, and how the new patterns introduced with React 18 aim to fix those problems.

Web vitals terminology

Before we dive into the rendering patterns, let’s look at some web vital terminology that we’ll be using throughout this post:

The current rendering patterns

Right now, the most common patterns we use in React are Client-side rendering and Server Side rendering and some advanced forms of server rendering offered by frameworks like Next.js, Static Site Generation, and Incremental Static Regeneration. We’ll look into each of those and dive into the newer patterns introduced with React 18 and available in the future.

Client-Side Rendering (CSR)

Client-side rendering, mostly with create-react-app or other similar starters, was the default way for building React apps for some time before meta-frameworks like Next.js and Remix came along.

With CSR, the server only provides barebones HTML for every page containing the necessary script and link tags. Once the relevant JavaScript is downloaded to the browser. React renders the tree and generates all the DOM nodes. All the logic for routing and data fetching is handled by the client-side JavaScript as well.

To see how it works, let’s consider that we are rendering the below app:

// App.jsx

<Layout>
	<Navbar />
	<Sidebar />
	<RightPane>
		<Post />
		<Comments />
	</RightPane>
</Layout>

This is what the render cycle of the above app will look like:

And hence this is the network graph:

Client side rendering network graph

So CSR apps have a fast time to first byte because they rely on mostly static assets. However, users must stare at a blank screen until the relevant JavaScript is downloaded. Even after that, most real-world apps would need to fetch data from an API to display relevant data to the users, which leads to a very slow LCP.

Advantages of CSR

Problems with CSR:

Server-side rendering

The way that server-side rendering works in React right now is:

To better understand how it works, let’s look at the lifecycle of the same app that we saw in the previous section with SSR:

// App.jsx

<Layout>
	<Navbar />
	<Sidebar />
	<RightPane>
		<Post />
		<Comments />
	</RightPane>
</Layout>

And hence this leads to the following network graph:

Server side rendering network graph

So with SSR, we get a good FCP and LCP, but the TTFB suffers because we have to fetch data on the server and then convert it to HTML string.

Now you might ask where Next.js’ SSG/ISR fits here. They also have to go through the same process we saw above. The only difference is that they don’t suffer from a slow Time To First byte because the HTML is either generated at build time or is generated and cached incrementally as requests come in.

Static Site generation/Incremental Static regeneration network graph

But SSG/ISR is not a silver bullet; in my experience, they work best for public pages but for pages that change based on the user’s logged-in status or some other cookies stored on the browser, you would have to use SSR.

Advantages of SSR

Problems with SSR

The new rendering patterns

In the previous section, we saw that what are the current rendering patterns in React and what are the problems with them. To summarize:

The React team is working on some new patterns that aim to solve these problems.

Streaming SSR

One cool thing about browsers is that they can receive HTML via HTTP streams. Streaming allows the web server to send data to a client over a single HTTP connection that can remain open indefinitely. So you can load data on the browsers over a network in multiple chunks, which are loaded out of order parallel to rendering.

Streaming before React 18

Streaming rendering isn’t something brand new in React 18. In fact, it has existed since React 16. React 16 had a method called renderToNodeStream which, unlike renderToString, rendered the frontend as an HTTP stream to the browser.

This allowed you to send down HTML in chunks parallel to rendering it, enabling a faster time to first byte and largest contentful paint for your users since the initial markup arrives in the browser sooner.

Max Stoiber illustrated the difference nicely in his article about how they used it at Spectrum.

Max Stoiber's illustration of how renderToNodeStream is different from renderToString

Streaming SSR with React 18

React 18 deprecates the renderToNodeStream API in favor of a newer API called renderToPipeableStream, which unlocks some new features with Suspense that allow you to break down your app into smaller independent units that can go through the steps that we saw with SSR independently. This is possible because of two major features added with Suspense:

Streaming HTML on the server

As mentioned previously, the SSR before React 18 was an all or nothing approach; first, you needed to fetch any data requirements that the page had, generate the HTML, and then send it to the client. This is no longer the case, thanks to HTTP streaming.

The way it will work with React 18 is that you can wrap the components that might take a longer time to load and are not immediately required on screen in Suspense. Again to understand how it works, let’s assume that the Comments API is slow, so we wrap the Comments component in Suspense.

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

This way, comments are not there in the initial HTML, and the user gets the fallback spinner in its place initially:

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

Finally, when the data is ready for Comments on the server, React will send minimal HTML to the same stream with an inline script tag to put 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>

Hence this solves the first problem since we now don’t have to wait for all the data to be fetched on the server and the browser can start rendering the rest of the app, even if some parts are not ready.

Selective hydration

Even though the HTML got streamed, the page won’t be interactive unless the whole JavaScript for the page is downloaded. That’s where selective hydration comes in.

One way to avoid large bundles on a page during client-side rendering was code-splitting via React.lazy. It specifies that a particular piece of your app doesn’t need to load synchronously, and your bundler would have split it off into a separate script tag.

The limitation with React.lazy was that it doesn’t work with server-side rendering, but now with React 18, <Suspense> apart from allowing you to stream the HTML, it also lets you unblock hydration for the rest of the app.

So now, React.lazy works out of the box on the server. When you wrap your lazy component in <Suspense>, you not only tell React that you want it to be streamed but also allow the rest of hydrated even if the component wrapped in <Suspense> is still being streamed. This also solves the second problem that we saw in traditional server-side rendering. You no longer have to wait for all the JavaScript to be downloaded before you start hydrating.

Again, let’s look at the lifecycle of the app with Comments wrapped in Suspense, and using the new Suspense Architecture

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

This leads to a network graph that looks something like this:

Streaming SSR with Suspense network graph

Again this is a very contrived example, but what it is trying to demonstrate is that with Suspense a lot of stuff that was happening serially now happens in parallel.

Which helps us in not only getting a faster time to first byte since the HTML was streamed but also, the users don’t have to wait for all the JavaScript to be downloaded to be able to start interacting with the app. Other benefits about streaming rendering that I could not include in this network graph is that it also helps load other assets (CSS, JavaScript, fonts, etc.) as soon as the page starts streaming, helping parallelizing even more requests, as Ryan mentioned in his talk.

Another cool thing is that if you have multiple components wrapped in Suspense and haven’t been hydrated on the client yet, but the user starts interacting with one of them, React will prioritize hydrating that component first. You can check this out and all the things discussed above in more detail in the New Suspense Architecture discussion.

Server components (Alpha)

In the last section, we saw how we could improve the server-side rendering performance by breaking our app into smaller units and streaming and hydrating them separately. But what if there was a way where you didn’t need to hydrate parts of your application at all?

Well, this is where the new Server Components RFC comes in which is meant to compliment server-side rendering, allowing you to have components that only render on the server and have no interactivity.

To give a brief overview, the way they work is that you can create non-interactive server components with .server.js/jsx/ts/tsx extensions, and they can then seamlessly integrate and pass props to client components (with .client.js/jsx/ts/tsx extensions) which can handle the interactive parts of the page. Here is a more detailed list of the features that they offer:

Now let’s check out how the app we have been looking at throughout this article would look with React Server Components. This time the Sidebar and the Post are server components, and the Navbar and the Comments section are client components. We have also wrapped the Post in Suspense as well.

<Layout>
  <NavBar />
  <SidebarServerComponent />
  <RightPane>
    <Suspense fallback={<Spinner />}>
      <PostServerComponent />
    </Suspense>
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

The network graph for this would be very similar to the streaming rendering with Suspense one with way less JavaScript.

So server components are even a further step to fix the problems that we started with, and they not only help us download way less JavaScript but also significantly improve the developer experience.

The React team also mentioned in the RFC FAQs that they ran an experiment with a small number of users on a single page at Facebook and they have with encouraging results with ~30% product code size reduction.

When can you start using these features?

Update

React Server Components are stable now and can be used in production with the Next.js app directory with v13.4 and later. You can read the Next.js docs for more info and also checkout my new blog where I compare building a Twitter Clone with Next.js and React Server Components and Remix.

Original answer

Well, Not yet.

You can try some demos that I have linked in the next section, but at the time of writing this article, Server Components are still in alpha, and Suspense for data fetching, which is needed for the Streaming SSR with new Suspense architecture, still isn’t official yet and will be released with a minor update of React 18.

And the React team also mentioned that all this stuff would be pretty complex so, they expect the initial adoption would be through frameworks. So you should keep an eye out for the Next.js Layouts RFC, which they are saying will be the biggest update to Next.js since its inception, and it will be built on top of the new React 18 features we discussed in this article.

Demos

You can check out the demos by the React team and the Next.js teams here:

References

Here are some of the references I used while writing this article:

Newsletter

Subscribe to get all new posts straight to your inbox. No spam ever. Unsubscribe at any time. Or subscribe via RSS if emails aren't your thing.

Comments