← Back to all blogs
Server Side Rendering Explained – Real World Example
Sat Feb 28 20269 minIntermediate

Server Side Rendering Explained – Real World Example

A comprehensive guide to Server Side Rendering, covering theory, architecture, a practical React/Node example, FAQs, and implementation tips.

#ssr#react#next.js#seo#web performance

What Is Server Side Rendering?

What Is Server Side Rendering?

Server Side Rendering (SSR) refers to the technique of generating the full HTML markup of a web page on the server, then delivering that pre‑rendered content to the browser. Unlike traditional client‑side rendering (CSR), where JavaScript runs in the browser to build the UI, SSR sends a ready‑to‑display page, allowing search‑engine crawlers and users on slow networks to see meaningful content instantly.

Why SSR Matters for SEO

Search engines traditionally struggle with JavaScript‑heavy pages because they may not execute all scripts before indexing. By rendering the page on the server, you guarantee that the <title>, meta tags, and structured data are present in the initial HTML payload. This improves crawlability, keyword relevance, and ultimately organic rankings.

When To Consider SSR

  • High‑traffic marketing sites where search visibility drives conversions.
  • E‑commerce product pages that need instant price and inventory visibility.
  • Applications where first‑paint performance is critical (e.g., news portals, dashboards).

While SSR offers clear benefits, it also introduces complexity, especially around data fetching, caching, and state hydration. The sections that follow break down the architecture, walk through a real‑world implementation, and discuss trade‑offs.

Architecture Overview

Implementing SSR requires a clear separation of concerns between the server, the rendering engine, and the client. Below is a high‑level diagram of a typical SSR stack:

Client (Browser) <-- HTTP Request --> Node/Express Server <-- React Renderer --> HTML

1. Request Flow

  1. Browser requests a URL - The server receives the HTTP GET request.
  2. Data fetching layer - The server invokes APIs or database queries to gather the data required for the page.
  3. React rendering - Using ReactDOMServer.renderToString(), the server transforms the component tree into a string of HTML.
  4. HTML response - The server injects the rendered markup into an HTML template, adds initial state (window.__INITIAL_DATA__), and sends it back.
  5. Hydration on the client - When the browser loads the HTML, React re‑initialises the component tree using the same data, attaching event listeners without re‑rendering the UI.

2. Key Components

  • Express (or any Node HTTP framework) - Handles routing, middleware, and serves the rendered HTML.
  • ReactDOMServer - Provides APIs like renderToString and renderToStaticMarkup.
  • Data Layer - Could be a REST service, GraphQL endpoint, or direct DB call. It must be serialisable to JSON for the client to consume.
  • Template Engine - Simple string interpolation or a library like ejs/pug to embed the rendered markup.

3. Caching Strategies

Because SSR can be CPU‑intensive, many production setups cache the HTML output at different levels:

  • Edge cache (CDN) - Stores the final HTML for popular routes.
  • Server‑side in‑memory cache - Caches API responses for a short TTL.
  • Static generation fallback - For pages that seldom change, pre‑render them at build time (Hybrid SSR/Static).

Understanding this flow helps you diagnose performance bottlenecks, such as slow API calls or unnecessary re‑rendering of large component trees.

Real‑World Example: React + Express SSR

Real‑World Example: React + Express SSR

Below is a minimal, production‑ready example that demonstrates SSR with a React front‑end and an Express back‑end. The code is split into three files for clarity: server.js, App.jsx, and template.html.

1. server.js (Node/Express)

// server.js
const express = require('express');
const path = require('path');
const fs = require('fs');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const App = require('./App').default; // Babel compiled

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

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

// Simple API endpoint used by SSR app.get('/api/message', (req, res) => { res.json({ message: 'Hello from the server API!' }); });

// Catch‑all route for SSR pages app.get('*', async (req, res) => { // 1️⃣ Fetch data needed for the page const apiResponse = await fetch(http://localhost:${PORT}/api/message); const data = await apiResponse.json();

// 2️⃣ Render React to a string with the fetched data as props const appHtml = ReactDOMServer.renderToString( React.createElement(App, { initialData: data }) );

// 3️⃣ Read the HTML template and inject the markup + state const template = fs.readFileSync(path.resolve('./template.html'), 'utf8'); const html = template .replace('<!--APP-->', appHtml) .replace('/INITIAL_DATA/', window.__INITIAL_DATA__ = ${JSON.stringify(data)};);

// 4️⃣ Send the fully rendered page res.send(html); });

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

2. App.jsx (React Component)

jsx // App.jsx import React, { useEffect, useState } from 'react';

function App({ initialData }) { // ① Use the server‑provided data for the first render const [message, setMessage] = useState(initialData?.message || 'Loading...');

// ② After hydration, you may want to refetch or update data client‑side useEffect(() => { // Only fetch again if the data is stale (demonstration purpose) if (!initialData) { fetch('/api/message') .then(res => res.json()) .then(data => setMessage(data.message)); } }, []);

return ( <div style={{ fontFamily: 'Arial, sans-serif', padding: '2rem' }}> <h1>Server Side Rendering Demo</h1> <p>{message}</p> </div> ); }

export default App;

3. template.html (HTML Shell)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>SSR Demo - React & Express</title>
  <meta name="description" content="A real‑world example of Server Side Rendering using React and Express." />
  <link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
  <div id="root"><!--APP--></div>
  <script>
    /*INITIAL_DATA*/
  </script>
  <script src="/static/bundle.js"></script>
</body>
</html>

How It Works

  1. The server fetches data from /api/message before rendering.
  2. ReactDOMServer.renderToString creates a markup string that is inserted into the <!--APP--> placeholder.
  3. The same data is serialized into window.__INITIAL_DATA__ so the client can hydrate without a flash of empty content.
  4. After the page loads, React re‑mounts on #root, re‑uses the initial state, and attaches event listeners.

Build Steps (Babel & Webpack)

bash

Install dependencies

npm i express react react-dom isomorphic-fetch npm i -D @babel/core @babel/preset-env @babel/preset-react webpack webpack-cli babel-loader

Sample webpack.config.js (client bundle)

module.exports = { entry: './src/client.jsx', output: { path: __dirname + '/public', filename: 'bundle.js' }, module: { rules: [{ test: /.jsx?$/, use: 'babel-loader', exclude: /node_modules/ }] }, resolve: { extensions: ['.js', '.jsx'] }, mode: 'production' };

After compiling the client bundle, start the server with node server.js. Navigate to http://localhost:3000 and you will see the pre‑rendered HTML instantly, with React taking over once the script loads.

Benefits, Limitations, and Best Practices

Benefits of Server Side Rendering

  • Improved SEO - Search engines receive fully populated markup, allowing better indexing of keywords, Open Graph tags, and schema.org data.
  • Faster First Contentful Paint (FCP) - Users see meaningful UI within the first network round‑trip, crucial for low‑end devices and slow connections.
  • Reduced Time‑to‑Interactive (TTI) - Because the HTML is already present, the browser can start painting while JavaScript bundles load in the background.
  • Social Media Previews - Platforms like Facebook and Twitter scrape the initial HTML for preview data, ensuring rich link cards.

Common Limitations

  • Increased Server Load - Rendering React on each request consumes CPU and memory; scaling may require additional instances or serverless functions.
  • Complex State Management - Keeping server and client state in sync (hydration) can be tricky, especially with authentication tokens or mutable data.
  • Longer Build Pipelines - You often need Babel/Webpack for both client and server bundles, adding configuration overhead.
  • Caching Overhead - Without proper caching, repeated SSR calls can degrade performance; implementing CDN edge caching adds architectural complexity.

Best Practices

  1. Separate Data Layer - Abstract API calls into reusable services. This makes it easier to share logic between client and server.
  2. Use renderToStaticMarkup for Non‑Interactive Pages - For purely informational pages (e.g., blog posts), avoid the extra React attributes to reduce payload size.
  3. Implement Incremental Static Regeneration (ISR) where possible - Frameworks like Next.js allow you to statically generate a page at build time and re‑validate it on demand, combining the speed of static files with SSR freshness.
  4. Leverage Streaming SSR - Modern React (ReactDOMServer.renderToPipeableStream) can stream chunks of HTML to the client, decreasing Time‑to‑First‑Byte (TTFB).
  5. Monitor Performance - Use tools like Lighthouse, WebPageTest, and server‑side profiling to track FCP, TTFB, and CPU usage.
  6. Secure Hydration Data - Sanitize any data inserted into window.__INITIAL_DATA__ to prevent XSS attacks.

When to Combine SSR with Client‑Side Rendering

Hybrid approaches let you render the initial view on the server and let the client take over for subsequent interactions. This pattern-often called Universal or Isomorphic rendering-provides the SEO advantages of SSR while preserving the rich interactivity of SPA navigation.

FAQs

1️⃣ Does Server Side Rendering replace client‑side rendering?

No. SSR primarily handles the first page load. After hydration, the client continues to manage UI updates, routing, and state changes just like a traditional single‑page application.

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

Absolutely. Vue (Nuxt.js), Angular (Angular Universal), Svelte (SvelteKit), and even plain JavaScript libraries can be rendered on the server. The core concepts-data fetching, HTML generation, and hydration-remain the same.

3️⃣ How does SSR affect caching and CDN usage?

SSR pages can be cached at the edge (CDN) based on URL, query parameters, or HTTP headers. For dynamic content, employ short‑TTL caching or stale‑while‑revalidate strategies to balance freshness and performance.

4️⃣ Is SSR compatible with authentication flows?

Yes, but you must handle cookies or tokens securely on the server. Render the page based on the authenticated user’s context, then pass a sanitized token to the client for subsequent API calls.

5️⃣ What are the costs of running SSR in production?

The primary costs are additional server compute and memory. Using serverless platforms (AWS Lambda, Vercel) can auto‑scale based on traffic, but you should monitor cold‑start latency for high‑traffic pages.

Conclusion

Server Side Rendering bridges the gap between SEO‑friendly static markup and the dynamic, component‑driven experience that modern users expect. By moving the initial render to the server, you gain faster first paints, better crawlability, and a smoother experience on low‑bandwidth devices. The real‑world example presented-React coupled with an Express server-demonstrates how a few concise steps can deliver a fully hydrated application without sacrificing interactivity.

However, SSR is not a silver bullet. It introduces server load, adds complexity to state management, and requires thoughtful caching strategies. Treat SSR as part of a broader performance toolkit: combine it with static generation for evergreen pages, employ streaming for large payloads, and always monitor key metrics (FCP, TTFB, CPU usage).

When implemented correctly, SSR gives you the best of both worlds: the discoverability of static sites and the dynamism of single‑page applications. Start with the example provided, experiment with caching layers, and gradually adopt advanced patterns like incremental static regeneration. Your users-and search engines-will thank you.