Skip to main content

Overview

Server-side rendering (SSR) is a technique for rendering React components on the server and sending fully formed HTML to the client. This approach improves initial page load performance and SEO while enabling progressive enhancement strategies.
SSR shifts the rendering work from the client to the server, delivering a fully rendered page that users can see immediately while JavaScript loads in the background.

Rendering methods

React provides two primary methods for server-side rendering, each optimized for different use cases and performance characteristics.

renderToString

The traditional synchronous rendering method that generates a complete HTML string before sending any content to the client.Characteristics:
  • Synchronous, blocking operation
  • Waits for entire component tree to render
  • Returns complete HTML string
  • Simple to implement and reason about
Use cases:
  • Small to medium applications
  • Static content generation
  • When full HTML is needed before response
import { renderToString } from 'react-dom/server';
import App from './App';

const html = renderToString(<App />);
res.send(`
  <!DOCTYPE html>
  <html>
    <body>
      <div id="root">${html}</div>
      <script src="/bundle.js"></script>
    </body>
  </html>
`);
renderToString blocks the server until the entire component tree is rendered, which can cause latency issues for large applications.

Progressive hydration

Progressive hydration is a technique that allows parts of the page to become interactive at different times, rather than waiting for the entire application to hydrate.
1

Initial HTML render

The server sends fully rendered HTML to the client. Users can see the content immediately, even before JavaScript loads.
2

Selective script loading

JavaScript bundles are loaded based on priority. Critical components receive their scripts first, while less important parts load later.
3

Incremental hydration

As each component’s JavaScript arrives, it hydrates independently without blocking other components. Users can interact with hydrated parts while others are still loading.
4

Full interactivity

Once all components have hydrated, the application is fully interactive with all client-side functionality available.

Benefits

  • Faster Time to Interactive (TTI): Users can interact with critical parts of the page sooner
  • Better perceived performance: The page feels responsive even during hydration
  • Reduced JavaScript blocking: Heavy components don’t block lighter ones from becoming interactive
  • Optimized resource usage: JavaScript execution is spread over time

Selective hydration

Selective hydration builds on progressive hydration by prioritizing which components to hydrate based on user interaction and component importance.
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <Header />
      <Suspense fallback={<Spinner />}>
        <HeavyComponent />
      </Suspense>
      <Footer />
    </div>
  );
}

How it works

1

Suspense boundaries

Components wrapped in Suspense boundaries can hydrate independently. The server sends HTML for all components, but JavaScript loads on demand.
2

Priority-based hydration

React automatically prioritizes hydration based on user interactions. If a user clicks on a component that hasn’t hydrated yet, React fast-tracks its hydration.
3

Concurrent rendering

Using React’s concurrent features, hydration work can be interrupted and resumed, ensuring the browser remains responsive to user input.
Key insight: Selective hydration allows React to respond to user interactions even before the entire page has hydrated, creating a more responsive user experience.

Comparison matrix

FeaturerenderToStringrenderToPipeableStream
ExecutionSynchronousAsynchronous
TTFBSlower (waits for complete render)Faster (streams chunks)
Suspense supportNoYes
Progressive hydrationLimitedFull support
Server blockingBlocks until completeNon-blocking
Use caseSimple apps, static generationComplex apps, dynamic content

Best practices

Use streaming for large apps

Prefer renderToPipeableStream for applications with complex component trees or varying data load times to improve TTFB.

Implement Suspense boundaries

Wrap heavy components in Suspense to enable selective hydration and prevent them from blocking the entire page.

Prioritize critical content

Ensure above-the-fold content is in the shell that renders first, while deferring below-the-fold content to later chunks.

Monitor performance metrics

Track TTFB, FCP, and TTI to measure the effectiveness of your SSR strategy and identify optimization opportunities.

Build docs developers (and LLMs) love