//

Server Side Rendering with Suspense in React 18

React 18 will include architectural improvements to React server-side rendering (SSR) performance.

Server-side rendering is changing in React 18. React 18 will include architectural improvements to React server-side rendering (SSR) performance. This is supposed to solve the following issues we are currently having today:

React 18 will include architectural improvements to React server-side rendering (SSR) performance. This suppose to solve follwing issues we are currently having today:

  1. Fetch all data before rendering
  2. Load every JS for every component before hydrating HTML
  3. Hydrate everything before interacting

Current implmentaions of server-side rendering, Next.js for example are having to wait for the API calls to be resolved before rendering HTML and sent to the client. This slows down the intial page load for the client-side.

API Changes

Previously, React did not support Suspense on the server at all. This is changing in React 18

  • renderToString: Keeps working (with limited Suspense support).
  • renderToNodeStream: Deprecated (with full Suspense support, but without streaming).
  • pipeToNodeWritableNew and recommended (with full Suspense support and streaming).

Let's look at these APIs individually.

Let's look at these APIs individually.

renderToString

Render a React element to its initial HTML. React will return an HTML string. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes. This is synchronous and can't wait any asynchronous processes.

React 18: Limited <Suspense> support

The nearest Suspense boundary will be marked as "client-rendered" and immediately emit the fallback HTML in server-side.

On the client however, after the the JavaScript has loaded, React will retry rendering its content.

import React from 'react';
import { renderToString } from 'react-dom/server';

function App() {
  const html = renderToString(<>Hey there!</>);
  
  return <div dangerouslySetInnerHTML={{__html: html}}></div>;
};

export default App;

renderToNodeStream

This function works as same as renderToString() above. The difference  is renderToNodeStream will have renderer extended from stream.Readable and it produces an HTML stream instead of a string.

This means that you can send the rendered HTML to the client byte-by-byte during rendering, contrary to the standard renderToString, when you have to wait for the whole HTML string to be rendered first, and only after can you send it to the client.

React 18: This will be deprecated in React 18 and will warn you if you use it. The new API pipeToNodeWritable is recommended instead.

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';

const app = express();

app.use('/static', express.static('public'));

const App = (props) => {
    return <div>Hello {props.name}</div>;
};

const Html = (props) => {
    return (
        <html>
            <body>
                <div id="app">{props.children}</div>
                <script id="initial-data" type="text/plain" data-json={props.initialData}></script>
                <script src="/static/bundle.js"></script>
            </body>
        </html>
    );
};

const initialData = {
    name: 'World'
};

app.get('/', (req, res) => {
    ReactDOMServer.renderToNodeStream(
        <Html initialData={JSON.stringify(initialData)}>
            <App {...initialData} name="World!" />
        </Html>
    ).pipe(res);
});

app.listen(8080, () => {
    console.log('listening on port 8080...');
});

pipeToNodeWritable

This will be the recommended API going forward. It has all the new features:

  • Full built-in support for <Suspense> (which integrates with data fetching)
  • Code splitting with lazy without flashes of "disappearing" content
  • Streaming of HTML with "delayed" content blocks "popping in" later
import { pipeToNodeWritable } from 'react-dom/server';

Here's a CodeSandBox example from the React team:

pipeToNodeWritable

Advantages using SSR with Suspense

First let's look at the advantages of SSR iteself:

  • Bundle size - Reduce bundle size by moving static, render-only components into the server-side and keep the interactive-stateful components on the client-side
  • Server components can access server-side resources. You can fetch data from the database or the filesystem directly, and you can also fetch from APIs just like on the client-side
  • Server components can read REST APIs as well as GraphQL queries asynchronously

By updating to React 18, we also get the following advantages:

  • Streaming HTML
  • SSR with Suspense
  • Selective hydration

Streaming HTML

Stream parts of an HTML page and increase the page load time without having to wait for the whole tree to be loaded before streaming the HTML.

To opt into it, you’ll need to switch from renderToString to the new pipeToNodeWritable method.

Server-side Suspense

By wrapping a component with Suspense, React will asynchonously render the component displaying the fallback component and later replace it with the component and hydrate it. This is done by sending the remaining code via a chunk rendered with a script tag

<Suspense fallback={<Loading />}>
   <MyComponent />
</Suspense>

Selective hydration

Before React 18, everything needed to be loaded before the hydrate step happens. Then only will React hydrate the HMTL before making it interactive (handling events etc).

By using Suspense to wrap your components, other parts of the page will be available for interaction.

By interacting with a hydrating component, for example clicking on a component that is still loading, React can prioritise the area. React does this by recording that interaction and replaying it once the component has finished hydrating.