Client-Side Rendering
In a standard React application, the browser receives an empty HTML from the server along with the JavaScript instructions to construct the UI. This is called Client-Side Rendering (CSR) because the initial rendering work happens on the user's device.
This process of rendering your components and attaching event handlers is known as “hydration”.
Or As per Dan Abramov
It’s like watering the “dry” HTML with the “water” of interactivity and event handlers.
However, CSR comes with well know issues associated with it as in,
- The web page rankings suffer in the search engine index as an empty HTML is sent to the client initially that makes it difficult for the web crawlers to identify the content of the web page.
- Initial load time of application is slow as complete Javascript has to be downloaded on the client first. If the client’s network is slow then it may even hang the browser for few seconds in the initial load.
Server-Side Rendering
To counter above mentioned issues, React provides another approach, Server-Side Rendering (SSR), where the rendering of HTML happens on the server itself instead of the client, and sends that HTML to our users.
SSR in React happen in following 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.
This indicates following major issues when working with SSR today:
- All the data on the server should be collected before the server can start sending any HTML to the client because SSR does not allow components to “wait for data”.
- All the Javascript should be loaded for all the components on the client before React can start the Hydration because the tree produced on the server must match the tree produced on the client otherwise React will remove (during hydration) a chunk of HTML whose Javascript is not yet loaded.
- Every component on the client should be hydrated before user can start interacting with the web page.
Suspense to the rescue!
Previously, React did not support Suspense on the server, and it was only limited for lazy-loading the code on the client, this is changing in React 18.
There are two major SSR features in React 18 unlocked by Suspense:
- Streaming HTML on the server
- Selective Hydration
Streaming HTML on the server
With today’s SSR, rendering HTML and hydration is “all or nothing” process.
First you render all HTML on the server.
<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 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></Supense>
.
Let’s wrap the comment block and tell React that until it’s ready, React should display the <Spinner />
component:
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
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
.
Moreover, 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's HTML is ready, React will stream it. There is no requirement that data loads in any particular order. You specify where the spinners should appear, and React figures out the rest.
Selective Hydration
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" in other words 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:
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
Previously, this did not work with server rendering.
But in React 18, <Suspense>
lets you hydrate the other sections of the app before the comment widget has loaded.
This is an example of Selective Hidration 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 i.e. 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.
Interacting with the page before all the components have hydrated
Last and but not the least, 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.
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:
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
Now both of them (Sidebar and Comments) can be streamed from the server after initial HTML containing other components are streamed to the client.
But this also has a consequence on hydration as well. Let’s say the HTML for both Sidebar and Comments has loaded but the code is not loaded yet. As soon as the code loads, React will start the hydration process starting with the first Suspense boundary it encounters, which is Sidebar in out case.
Now let’s say the user starts interacting with the Comments HTML which the hydration of Sidebar is going on,
Here React will synchronously hydrate the comments during the capture phase of the click event (How cool is that!).
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.
React starts hydrating everything as early as possible, and it prioritizes the most urgent part of the screen based on the user interaction.
This resolves the third problem, no need to wait for all components to hydrate before use can start interacting with the page. Selective Hydration will prioritize the components the user is interacting with, and hydrate them early.
Thank you for reading!
I hope this post was useful to you and helped you understand why Suspense is more than just a fancy loading spinner! 😉
This is just a high-level overview of the new SSR architecture and what problems Suspense solve, however if you want to dive deeper into this concept, you can read this superb discussion thread explained by React's team itself.
PS: This post was inspired by the discussion.
Feel free to reach out to me! 😊
Top comments (0)