Server Side Rendering in React: A Complete Technical Guide to SSR vs CSR

Server side rendering in React generates static HTML on the server using ReactDOMServer.renderToString or renderToReadableStream, delivering immediate first paint and SEO benefits, while client side rendering builds the DOM entirely in the browser after downloading JavaScript.

Server side rendering in React is a technique where components are executed on the server to produce HTML markup before reaching the browser. According to the facebook/react source code, this approach leverages the react-dom/server APIs to pre-render applications, contrasting sharply with client side rendering where React constructs the user interface entirely within the browser environment.

What Is Server Side Rendering in React?

Server side rendering (SSR) is the process of executing React components on the server to generate static HTML markup. The server runs ReactDOMServer.renderToString (or renderToStaticMarkup) to produce a string of HTML that represents the initial UI state. This pre-rendered HTML is sent to the browser, allowing users to see content immediately without waiting for JavaScript to download and execute.

Once the HTML arrives, React performs hydration—a process where ReactDOM.hydrateRoot attaches event listeners and reconciles the server-generated markup with the client-side React tree. This transforms the static page into a fully interactive application.

How Client Side Rendering Works

Client side rendering (CSR) delivers a minimal HTML shell—often just an empty <div id="root"></div>—along with script tags pointing to JavaScript bundles. The browser downloads these bundles, parses the code, and React executes createRoot to build the component tree entirely within the client. Only after this execution does the user see meaningful content.

This approach shifts all rendering responsibility to the browser, eliminating the need for a Node.js server to generate markup but introducing latency before the first contentful paint.

Key Differences: SSR vs CSR

First Paint: SSR provides immediate visual feedback because HTML is pre-generated on the server, while CSR delays content display until the JavaScript bundle is fetched, parsed, and executed.

SEO: SSR offers strong search engine optimization because crawlers receive the final markup instantly without executing JavaScript. CSR requires bots to run JavaScript to see content, which modern crawlers handle but with less reliability than server-generated HTML.

Performance: SSR delivers faster Time-to-First-Byte (TTFB) and First Contentful Paint (FCP) at the cost of increased server compute. CSR provides faster subsequent interactions once the initial JavaScript loads, with no server rendering overhead per request.

Complexity: SSR requires a Node.js or Edge runtime environment that executes React, handles data fetching, and streams HTML responses. CSR simplifies deployment to static file hosting with no server-side execution environment needed.

Interactivity: SSR requires an explicit hydration step using ReactDOM.hydrateRoot to attach event listeners and make the UI interactive. CSR produces an interactive UI immediately after the JavaScript executes without a separate hydration phase.

Resource Usage: SSR places rendering costs on the server, increasing CPU and memory load for every request. CSR shifts rendering costs to the client device, with the server only responsible for serving static assets.

React SSR Implementation Details

React's server side rendering implementation resides in the react-dom package under the packages/react-dom/src/server directory. The architecture separates concerns between different runtime environments and rendering engines.

Server Entry Points

The framework provides environment-specific entry points to optimize for different deployment targets:

The Fizz Engine

React 18 introduced the Fizz concurrent rendering engine for server side rendering, implemented in packages/react-dom/src/server/ReactDOMFizzServerNode.js and packages/react-dom/src/server/ReactDOMFizzServerEdge.js. Fizz enables streaming SSR, allowing the server to send HTML progressively as components resolve, rather than waiting for the entire tree to render.

The renderToReadableStream function leverages Fizz to produce a Web Streams API ReadableStream, enabling efficient transmission of partial HTML before data fetching completes.

Client Hydration

On the client side, packages/react-dom/src/client/ReactDOMClient.js exports hydrateRoot, which attaches React's internal event system to server-generated markup. This function reconciles the existing HTML with the React component tree, preserving the DOM nodes while adding interactivity.

Practical Examples

Basic SSR with Express

The following example demonstrates server side rendering in React using Express and the renderToString API from packages/react-dom/src/server/react-dom-server.node.js:

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import App from './src/App';

const app = express();

app.get('*', (req, res) => {
  const html = renderToString(<App url={req.url} />);
  res.send(`
    <!doctype html>
    <html>
      <head><title>SSR Demo</title></head>
      <body>
        <div id="root">${html}</div>
        <script src="/static/client.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

Source reference: react-dom/src/server/react-dom-server.node.js

Hydrating on the Client

To make the server-rendered markup interactive, use hydrateRoot from packages/react-dom/src/client/ReactDOMClient.js:

import React from 'react';
import {hydrateRoot} from 'react-dom/client';
import App from './src/App';

hydrateRoot(document.getElementById('root'), <App />);

Source reference: react-dom/src/client/ReactDOMClient.js

Streaming SSR in React 18

For improved performance with Suspense, use renderToReadableStream implemented in the Fizz engine:

import express from 'express';
import React from 'react';
import {renderToReadableStream} from 'react-dom/server';
import App from './src/App';

const app = express();

app.get('*', async (req, res) => {
  const stream = await renderToReadableStream(<App url={req.url} />, {
    onShellReady() {
      // Send the shell while React continues streaming the rest
      const readable = stream.pipeThrough(new TextEncoderStream());
      readable.pipeTo(res);
    },
  });
});

app.listen(3000);

Source reference: react-dom/src/server/react-dom-server.node.js

Summary

  • Server side rendering in React generates HTML on the server using ReactDOMServer.renderToString or renderToReadableStream, enabling immediate content display and superior SEO performance.
  • The Fizz engine in packages/react-dom/src/server/ReactDOMFizzServerNode.js powers React 18's concurrent streaming capabilities, allowing progressive HTML transmission before data fetching completes.
  • Hydration via hydrateRoot from packages/react-dom/src/client/ReactDOMClient.js attaches event listeners to server-generated markup without re-rendering the DOM.
  • Client side rendering shifts all rendering to the browser, simplifying deployment but delaying content visibility until JavaScript executes.
  • Environment-specific entry points in packages/react-dom/src/server/ (node.js, edge.js, browser.js) optimize SSR for different runtime targets.

Frequently Asked Questions

What is the difference between renderToString and renderToReadableStream?

renderToString synchronously generates the complete HTML string for a React tree, blocking the server until rendering finishes. renderToReadableStream returns a Web Streams API ReadableStream that progressively emits HTML as components resolve, enabling the browser to start parsing and displaying content before the entire tree renders. According to the facebook/react source code, renderToReadableStream is implemented in the Fizz engine at packages/react-dom/src/server/ReactDOMFizzServerNode.js and is the recommended approach for React 18+ applications.

Does SSR improve SEO in React applications?

Yes, server side rendering significantly improves SEO because search engine crawlers receive fully-formed HTML immediately without executing JavaScript. While modern crawlers can process client-side JavaScript, SSR ensures that content is visible in the initial HTTP response, eliminating the risk of indexing delays or failures when bots encounter complex JavaScript bundles. This makes SSR essential for content-heavy applications where search visibility is critical.

What is hydration in React SSR?

Hydration is the process where React attaches event listeners and internal state to existing server-generated HTML markup. When using hydrateRoot from packages/react-dom/src/client/ReactDOMClient.js, React compares the server-rendered DOM against the virtual DOM tree, preserves the existing nodes to avoid re-rendering, and wires up interactivity. This step is necessary because the static HTML from the server contains no JavaScript event handlers; hydration transforms the static page into a dynamic React application without destroying the existing markup.

When should I use CSR instead of SSR?

Use client side rendering when your application requires complex user interactions without SEO constraints, such as internal dashboards or authenticated single-page applications where content is hidden behind login walls. CSR simplifies deployment to static hosting services like Vercel or Netlify without requiring a Node.js server, and provides faster subsequent navigation once the initial JavaScript bundle loads. Choose CSR over SSR when server compute costs outweigh the benefits of immediate first paint and SEO optimization.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →