← Back to all blogs
Server Side Rendering Explained – Step‑By‑Step Tutorial
Sat Feb 28 20269 minIntermediate

Server Side Rendering Explained – Step‑By‑Step Tutorial

A comprehensive, SEO‑optimized tutorial that demystifies server side rendering, provides real‑world code examples, and explains the underlying architecture.

#ssr#server side rendering#react#node.js#express#next.js#performance#seo

Introduction

What Is Server Side Rendering?

Server Side Rendering (SSR) is the technique of generating the complete HTML for a web page on the server, then sending it to the client’s browser. Unlike client‑side rendering (CSR) where JavaScript builds the UI after the page loads, SSR delivers a fully‑populated DOM on the first request. This approach improves initial load speed, search‑engine visibility, and social‑media sharing because crawlers receive ready‑to‑index content.

Why SSR Matters for Modern Web Apps

  • SEO advantage - Search bots can index content without executing JavaScript.
  • Faster Time‑to‑Content (TTC) - Users see meaningful information sooner, reducing bounce rates.
  • Better performance on low‑end devices - The server does the heavy lifting, leaving the client with a lighter JavaScript bundle.

In this tutorial we will walk through the SSR lifecycle, design a simple architecture, and implement a working example using React, Node.js, and Express. Advanced topics such as data pre‑fetching, streaming, and caching are also covered.

How SSR Works – Architecture Explained

High‑Level SSR Architecture

The classic SSR flow can be visualised as a series of coordinated steps:

+-----------+ HTTP Request +-----------+ | Browser | --------------------> | Server | +-----------+ +-----------+ ^ | | | | HTML Response (fully rendered) | | v +-----------+ HTTP Response +-----------------+ | Client | <-------------------| Rendering Engine | +-----------+ +-----------------+

  1. Incoming request - The browser requests a URL.
  2. Server-side router - The Node.js/Express router maps the URL to a React component tree.
  3. Data fetching - Before rendering, the server resolves all asynchronous data (e.g., API calls) required by the component.
  4. React renderToString - The component tree is rendered to a static HTML string.
  5. HTML template injection - The string is inserted into an HTML shell that includes meta tags, CSS links, and a script tag pointing to the client bundle.
  6. Response sent - The composed HTML is sent back to the browser.
  7. Hydration - On the client, the same React bundle mounts onto the server‑generated markup, activating interactivity.

Key Architectural Components

  • Express Server - Handles routing, data fetching, and serves static assets.
  • React Application - Stateless UI components that can be rendered on both server and client.
  • Data Layer - Typically a service layer (REST/GraphQL) that returns JSON. In SSR we call it synchronously during the server render.
  • HTML Template - A minimal index.html with placeholders for the rendered markup and initial state.

Diagram (ASCII) of a Full‑Stack SSR Setup

+--------------------------------------------------------------+ | Node.js (Express) | | +-------------------+ +-------------------+ +----------+ | | | Router Layer | | Data Service | | Cache | | | +---------+---------+ +---------+---------+ +----+-----+ | | | | | | | v v v | | +------------------------------------------------------+ | | | React SSR Renderer (renderToString) | | | +------------------------------------------------------+ | | | | | v | | +------------------------------------------------------+ | | | HTML Template (index.html) with {{markup}} | | | +------------------------------------------------------+ | | | | | v | | +------------------------------------------------------+ | | | HTTP Response (HTML) sent to client | | | +------------------------------------------------------+ | +--------------------------------------------------------------+

Understanding this stack helps you pinpoint where to optimise - whether it’s query caching, streaming HTML, or splitting bundles.

Implementing SSR with React and Express

Step‑by‑Step Code Walkthrough

Below is a minimal yet production‑ready implementation. We will create three files:

  • server.js - Express server with SSR logic.
  • src/App.jsx - Root React component.
  • public/index.html - HTML template.

1. Project Setup (CLI Commands)

bash

Initialize npm project

npm init -y

Install dependencies

npm install express react react-dom

Install dev dependencies for Babel and JSX support

npm install -D @babel/core @babel/preset-env @babel/preset-react @babel/register

Create a .babelrc file:

{ "presets": ["@babel/preset-env", "@babel/preset-react"] }

2. src/App.jsx

jsx import React from "react";

// Simulated data fetching function export async function fetchGreeting(name) { // In a real app this could be an API call return new Promise(resolve => { setTimeout(() => resolve(Hello, ${name}!), 100); }); }

export default function App({ greeting }) { return ( <main style={{ fontFamily: "Arial, sans-serif", padding: "2rem" }}> <h1>{greeting || "Loading..."}</h1> <p> This page is rendered on the <strong>server</strong> and then hydrated on the client. </p> </main> ); }

3. public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>SSR Demo</title>
</head>
<body>
  <div id="root"><!--{{SSR_CONTENT}}--></div>
  <script>
    // Preserve initial state for hydration
    window.__INITIAL_DATA__ = {{INITIAL_DATA}};
  </script>
  <script src="/static/client.bundle.js"></script>
</body>
</html>

4. server.js

// Enable ES6/JSX syntax on the fly
require("@babel/register")({ extensions: [".js", ".jsx"] });

const path = require("path"); const express = require("express"); const React = require("react"); const ReactDOMServer = require("react-dom/server"); const App = require("./src/App.jsx").default; const { fetchGreeting } = require("./src/App.jsx");

const app = express(); const PORT = process.env.PORT || 3000;

// Serve static assets (client bundle, CSS, etc.) app.use("/static", express.static(path.resolve(__dirname, "static")));

app.get("/:name?", async (req, res) => { const name = req.params.name || "World";

// 1️⃣ Fetch data before rendering const greeting = await fetchGreeting(name);

// 2️⃣ Render React component to a string const appHtml = ReactDOMServer.renderToString( React.createElement(App, { greeting }) );

// 3️⃣ Read the HTML template const templatePath = path.resolve(__dirname, "public/index.html"); let template = require("fs").readFileSync(templatePath, "utf8");

// 4️⃣ Inject the rendered markup and serialized state template = template.replace("<!--{{SSR_CONTENT}}-->", appHtml); template = template.replace( "{{INITIAL_DATA}}", JSON.stringify({ greeting }) );

// 5️⃣ Send the complete HTML page res.send(template); });

app.listen(PORT, () => { console.log(🚀 SSR server running at http://localhost:${PORT}); });

5. Client‑Side Hydration Bundle (Optional)

Create a simple client.jsx that re‑uses the same App component and hydrates the markup.

import React from "react";
import ReactDOM from "react-dom";
import App from "../src/App.jsx";

const initialData = window.INITIAL_DATA;

ReactDOM.hydrate( <App greeting={initialData.greeting} />, document.getElementById("root") );

Bundle it with a tool like Webpack or Vite to produce static/client.bundle.js. The server serves this file, enabling interactive UI after the initial HTML is displayed.

6. Running the Demo

bash node server.

Visit http://localhost:3000 or http://localhost:3000/Alice to see personalized greetings rendered on the server.

What Makes This Implementation SEO‑Friendly?

  • The <h1> containing the greeting is present in the raw HTML.
  • Meta information can be added dynamically in the template before the response is sent.
  • No reliance on client‑side JavaScript to fetch the initial data - crawlers read the content instantly.

Advanced Optimizations & Best Practices

Streaming SSR for Faster Perceived Load

Node.js 13+ supports React 18 streaming with renderToPipeableStream. Instead of waiting for the entire component tree, the server can flush chunks as soon as they are ready, reducing Time‑to‑First‑Byte (TTFB).

const { renderToPipeableStream } = require('react-dom/server');

app.get('/stream/:name?', async (req, res) => { const name = req.params.name || 'World'; const greeting = await fetchGreeting(name);

const { pipe } = renderToPipeableStream( React.createElement(App, { greeting }), { onShellReady() { res.setHeader('Content-Type', 'text/html'); pipe(res); }, onError(error) { console.error(error); } } ); });

The browser starts rendering the markup before the full React tree is resolved, delivering a smoother experience on slow networks.

Caching Rendered Pages

For pages that change infrequently (e.g., marketing landing pages), cache the HTML output in memory or a CDN. Example using lru-cache:

const LRU = require('lru-cache');
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 10 }); // 10‑minute TTL

app.get('/:name?', async (req, res) => { const cacheKey = req.originalUrl; if (cache.has(cacheKey)) { return res.send(cache.get(cacheKey)); } // …render as before… const html = template; // after injection cache.set(cacheKey, html); res.send(html); });

Critical CSS Inlining

Inject essential CSS directly into <head> during SSR to avoid render‑blocking external stylesheet loads. Tools like critters can automate extraction.

Error Handling & Fallbacks

Always provide a graceful fallback if data fetching fails. Render a static error page or a skeleton UI and let the client fetch the missing data after hydration.

Security Considerations

  • HTML escaping - When injecting serialized state (JSON.stringify) ensure characters like < are escaped to prevent XSS.
  • Content‑Security‑Policy (CSP) - Define a strong CSP header to restrict script sources.
  • Rate‑limiting - Protect SSR endpoints from abuse, as they execute server‑side code for each request.

FAQs

Frequently Asked Questions

1️⃣ Does SSR replace client‑side rendering entirely?

Answer: No. SSR provides the initial HTML, then the client bundle hydrates the markup to make it interactive. Subsequent navigation can be handled by client‑side routing (SPA behavior) or by full page reloads, depending on your architecture.

2️⃣ How does SSR impact server load?

Answer: Because the server renders each request, CPU and memory usage increase compared to serving static files. Mitigate this with caching, streaming, and off‑loading heavy data calls to micro‑services.

3️⃣ Can I use SSR with frameworks other than React?

Answer: Absolutely. Vue (vue-server-renderer), Angular Universal, SvelteKit, and SolidJS all support server‑side rendering. The core principles - data pre‑fetch, rendering to string, and hydration - remain consistent across frameworks.

4️⃣ Is SSR beneficial for non‑public pages (e.g., admin dashboards)?

Answer: It can be, especially for performance‑critical internal tools where fast first paint improves productivity. However, SEO gains are irrelevant for protected routes, so weigh the added complexity against the performance benefits.

5️⃣ How do I handle dynamic meta tags (title, description) in SSR?

Answer: Populate meta tags on the server before sending the HTML. Libraries like react-helmet-async allow you to collect head elements during rendering and inject them into the template.

Conclusion

Wrap‑Up: Why SSR Is a Strategic Choice

Server Side Rendering bridges the gap between SEO‑friendly static pages and the rich interactivity of modern JavaScript frameworks. By rendering the UI on the server, you gain:

  • Immediate content visibility for users and crawlers.
  • Reduced time‑to‑interactive through hydration rather than full client bootstrapping.
  • Scalable performance when combined with streaming, caching, and selective data pre‑fetching.

The step‑by‑step example above demonstrates that implementing SSR with React and Express is straightforward yet powerful. Extending this foundation with Next.js, Remix, or custom streaming pipelines can further optimise speed and developer experience.

Investing in SSR today prepares your application for the evolving demands of search engines, mobile devices, and user expectations. As the web continues to prioritize performance‑first metrics, SSR stands out as a resilient, future‑proof pattern for frontend engineers.