MENU
Server Rendering and Server Components
Everything so far has assumed the same starting point: the browser downloads an empty <div id="root">, then your JavaScript, and then React builds the page. For a dashboard behind a login that is the right answer. For a public catalog it is a liability â the first paint waits on a bundle, and the text that search engines and link previews want is not in the document they fetch.Server rendering moves the first render to Node. The server calls your components, turns the resulting element tree into HTML, and sends that HTML in the response; the browser paints it immediately and then hydrates it, attaching the event handlers that make it interactive. React Server Components go further and let some components run only on the server, never shipping their code or their dependencies to the browser at all.This section shows both from the bottom: a hand-rolled server, the streaming API that replaced renderToString, the static prerender that produces files for a CDN, and the model behind Server Components. Frameworks own this layer in practice, but they call exactly these functions, and Chapter 5 puts the practical version to work in Next.js.2.20.1 Why Render on the Server
A client-rendered page arrives empty. The browser parses a document whose body is one div, discovers a script, downloads it, parses it, runs it, and only then does React produce elements â nothing is on screen until that chain finishes, and it is as slow as the reader's connection and phone. A server-rendered page arrives with its markup in the response, so the browser paints on the first parse and the bundle catches up afterwards.
Figure 2.63: Client versus server rendering on one connection; grey
is work done on the serverBe precise about what that buys. The page paints sooner,
which is what a reader on a slow phone notices and what Largest
Contentful Paint measures. It becomes interactive slightly
later, because hydration is work a client-only page never does. Content
sits in the HTML, so crawlers, link unfurlers and text-only clients see
it without running scripts. And data can be fetched inside your data
center instead of over the reader's connection.The costs are equally concrete. You run a Node process that must stay
up, and rendering is CPU work that scales with traffic: a component
blocking for 300 ms blocks that request. Your components must survive an
environment with no window, no document and no localStorage, so code
touching them moves into an Effect or behind a guard. Session-specific
rendering fights with caching. And you ship components twice, as HTML
and as the bundle that hydrates them.2.20.2 renderToString and hydrateRoot
The smallest complete server-rendered application is a component, a server, a browser entry and no framework. app.jsx exports an ordinary App that takes a products prop and renders <h1>Catalog ({products.length} items)</h1>, a <ul> of names and prices, and a button whose onClick increments a useState counter. renderToString turns it into HTML synchronously; the rest of the server is plumbing â serve the bundle, embed the data the render used (data.js exports the two-product array), and link the bundle.import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { renderToString } from 'react-dom/server';
import App from './app.jsx';
import { products } from './data.js';
createServer(async (req, res) => {
if (req.url === '/client.js') {
res.writeHead(200, { 'Content-Type': 'text/javascript' });
return res.end(await readFile('./public/client.js'));
}
const html = renderToString(<App products={products} />);
const data = JSON.stringify(products).replace(/</g, '\\u003c');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<!doctype html><html><head><title>Catalog</title></head><body>
<div id="root">${html}</div><script id="data" type="application/json">${data}</script>
<script type="module" src="/client.js"></script></body></html>`);
}).listen(4000, () => console.log('SSR server on http://localhost:4000/'));
import { hydrateRoot } from 'react-dom/client';
import App from './app.jsx';
const products = JSON.parse(document.getElementById('data').textContent);
hydrateRoot(document.getElementById('root'), <App products={products} />);
echo '{ "compilerOptions": { "jsx": "react-jsx", "allowJs": true } }' > tsconfig.json
npx esbuild client.jsx --bundle --minify --format=esm --outfile=public/client.js
node --import tsx server.jsx
<!doctype html><html><head><title>Catalog</title></head><body>
<div id="root"><main><h1>Catalog (<!-- -->2<!-- --> items)</h1><ul><li>Keyboard<!-- --> - $<!--
-->79</li><li>Mouse<!-- --> - $<!-- -->29</li></ul><button>In cart: <!-- -->0</button></main>
</div><script id="data" type="application/json">[{"id":1,"name":"Keyboard","price":79},...]
</script><script type="module" src="/client.js"></script></body></html>
2.20.3 Streaming with renderToPipeableStream
renderToString makes the reader wait for the slowest part of the page. renderToPipeableStream does not: it sends the shell â everything outside your Suspense boundaries â as soon as it is ready, keeps the connection open, and pushes each boundary's markup down the same response when its data arrives. React renders the whole document here, <html> included, and bootstrapModules injects the hydration script.import { createServer } from 'node:http';
import { Suspense, use } from 'react';
import { renderToPipeableStream } from 'react-dom/server';
const reviews = () =>
new Promise((ok) => setTimeout(() => ok(['Solid build', 'Fast delivery']), 1000));
const Reviews = ({ promise }) => <ul>{use(promise).map((t) => <li key={t}>{t}</li>)}</ul>;
const Page = () => (
<html lang="en">
<head><title>Keyboard</title></head>
<body>
<h1>Keyboard</h1>
<Suspense fallback={<p>Loading reviews...</p>}><Reviews promise={reviews()} /></Suspense>
</body>
</html>
);
createServer((req, res) => {
const { pipe } = renderToPipeableStream(<Page />, {
bootstrapModules: ['/client.js'],
onShellReady() { res.writeHead(200, { 'Content-Type': 'text/html' }); pipe(res); },
onShellError: () => res.writeHead(500).end('<h1>Server error</h1>'),
});
}).listen(4001, () => console.log('Streaming server on http://localhost:4001/'));
[+ 37 ms] <!DOCTYPE html><html lang="en"><head><link rel="modulepreload" href="/client.js"/>
<title>Keyboard</title></head><body><h1>Keyboard</h1><!--$?-->
<template id="B:0"></template><p>Loading reviews...</p><!--/$-->
<script type="module" src="/client.js" id="_R_" async=""></script>
[+1042 ms] <div hidden id="S:0"><ul><li>Solid build</li><li>Fast delivery</li></ul></div>
<script>$RB=[];$RV=function(a){...};$RC("B:0","S:0")</script>
[+1042 ms] </body></html>
2.20.4 Static Prerendering
If a page's data does not change between requests, rendering it per request is waste: render it once at build time, write the HTML to a file, let a CDN serve it. React ships that API in a separate module, react-dom/static â prerenderToNodeStream in Node, prerender for Web-stream runtimes.import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { prerenderToNodeStream } from 'react-dom/static';
import App from './app.jsx';
import { products } from './data.js';
const page = <html lang="en"><head><title>Catalog</title></head>
<body><App products={products} /></body></html>;
const { prelude } = await prerenderToNodeStream(page, { bootstrapModules: ['/client.js'] });
await pipeline(prelude, createWriteStream('public/index.html'));
2.20.5 React Server Components in Concept
Server rendering runs your components in both places. Server Components add a third: code that runs before bundling, at build time or per request, in an environment that is neither the client nor the SSR pass. It never reaches the browser, so such a component may await a database query, read a file, or import a 200 KB markdown parser without a byte of it being downloaded.
Figure 2.64: Where a Server Component's work happens, and what the
browser receivesDirectives mark the split. A file whose first line is 'use client' is
a boundary: that module and everything it imports are compiled into the
client bundle, and its components behave as they have all chapter â
state, Effects, onClick. Everything above the boundary stays on the
server, and there is no directive for it; a Server Component is simply
one that no client module imports. It may be async and await in its
body, and it may pass rendered JSX down as props or children, which is
how a server tree wraps interactive islands. 'use server' is different:
it marks Server Functions, which the client calls over the
network, not components.Two consequences follow: Server Components hold no state and attach
no handlers, and values crossing to the client must be serializable, so
passing a function or a class instance is an error you will meet
early.The machinery behind this â a bundler that understands the
directives, a route that serializes the payload, a client runtime that
requests it â is a framework's job. Chapter 5 builds real pages this way
in Next.js: where the boundary belongs, and how Server Functions replace
an API endpoint for forms.