Skip to content

SSR for SPAs — Server-Side Rendering for SEO and Performance

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about SSR for SPAs. We cover key concepts, practical examples, and best practices to help you master this topic.

SSR for SPAs renders the initial page on the server, sending fully-formed HTML to the browser for improved SEO, faster perceived load, and better user experience.

What You'll Learn

By the end of this tutorial, you will understand how SSR works with SPAs, the hydration process, when to use SSR, and how Next.js and Nuxt implement SSR for React and Vue SPAs.

Why It Matters

Pure client-side SPAs have two fundamental problems: poor SEO and slow initial content visibility. SSR solves both by sending complete HTML from the server. Users see content immediately, and search engines index the rendered content without executing JavaScript.

Real-World Use

A news SPA migrated from CSR to SSR with Next.js. Initial load time dropped from 4.2s to 1.8s. Pages indexed by Google went from 12 to 8,400 in the first month. Organic traffic increased by 300% because search engines could finally read article content.

CSR vs SSR Flow

CSR vs SSR Comparison
    CSR (Client-Side Rendering):
    Server → Empty HTML shell + JS bundle
    Browser downloads JS → executes → renders content
    User sees blank page until JS loads and runs

    SSR (Server-Side Rendering):
    Server → Fully rendered HTML (with content)
    Browser displays HTML immediately
    JS loads in background → hydrates (attaches events)
    User sees content immediately, interactivity comes later

Think of CSR like a take-and-bake pizza. You get raw ingredients (JavaScript bundle) and must cook it yourself before eating (wait for JS to render). SSR is like a ready-to-eat pizza — you get it fully cooked (HTML with content) immediately, and the dipping sauce (JavaScript for interactivity) comes alongside.

Basic React SSR

// server.js — Express + React SSR
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) => {
    // Render the React app to HTML string
    const html = renderToString(<App url={req.url} />);

    // Send the fully-formed HTML page
    res.send(`
        <!DOCTYPE html>
        <html>
        <head>
            <title>My SSR App</title>
            <link rel="stylesheet" href="/styles.css">
        </head>
        <body>
            <div id="root">${html}</div>
            <script src="/client.js"></script>
        </body>
        </html>
    `);
});

app.listen(3000);

Hydration

// client.js — Hydration on the client
import { hydrateRoot } from 'react-dom/client';
import App from './src/App';

// hydrateRoot reuses the server-rendered HTML
// and attaches event handlers
hydrateRoot(document.getElementById('root'),
    <App url={window.location.pathname} />
);

Next.js Pages Router (SSR)

// pages/users/[id].js — SSR with getServerSideProps
export async function getServerSideProps(context) {
    const { params, req } = context;
    const { id } = params;

    // Fetch data on the server for each request
    const response = await fetch(`https://api.example.com/users/${id}`);
    const user = await response.json();

    if (!response.ok) {
        return {
            notFound: true // Shows 404 page
        };
    }

    return {
        props: {
            user,
            timestamp: new Date().toISOString()
        }
    };
}

function UserPage({ user, timestamp }) {
    return (
        <div>
            <h1>{user.name}</h1>
            <p>Email: {user.email}</p>
            <p>Joined: {user.createdAt}</p>
            <small>Rendered at: {timestamp}</small>
        </div>
    );
}

export default UserPage;

When to Use SSR

const ssrDecision = {
    useSSR: {
        when: [
            'Content must be indexed by search engines',
            'Initial load speed is critical',
            'Social media previews (Open Graph) are important',
            'Content changes on every request (user-specific)'
        ],
        examples: ['E-commerce product pages', 'News articles', 'User profiles']
    },
    useCSR: {
        when: [
            'Content is behind authentication',
            'Highly interactive with frequent updates',
            'Minimal SEO requirements',
            'Internal tools and dashboards'
        ],
        examples: ['Admin panels', 'Real-time dashboards', 'Chat applications']
    },
    useHybrid: {
        when: [
            'Mixed content (public + authenticated)',
            'Some pages need SEO, others do not'
        ],
        examples: ['Marketing pages (SSR) + App (CSR)']
    }
};

Common Mistakes

  1. Accessing browser APIs during SSR. window, document, and localStorage do not exist on the server. Guard all browser-specific code with checks.
  2. Hydration mismatch. If the server-rendered HTML does not match the client-rendered Virtual Dom, React logs warnings and re-renders in the browser, defeating SSR. Ensure consistent data.
  3. SSR every page. Not every page needs SSR. Use static generation or client-side rendering for authenticated pages.
  4. Not handling 404 in SSR. The server should return proper HTTP status codes (404, 301) based on data availability.
  5. Blocking on data fetching. SSR delays the response until all data is fetched. Use streaming SSR or suspense boundaries.

Practice Questions

  1. What is the difference between CSR and SSR?
  2. How does hydration work in SSR?
  3. When should you use SSR instead of CSR?
  4. What causes hydration mismatches?
  5. How does getServerSideProps work in Next.js?

Challenge: Set up a basic React SSR server with Express. Create two pages: one with SSR (server-fetched data) and one with CSR (client-fetched data). Compare initial load HTML and measure time-to-content.

FAQ

Does SSR improve SEO for all search engines?

SSR provides fully-rendered HTML that all search engines can index. Google can index JavaScript but not consistently. Bing and Yandex have limited JS support.

Is SSR slower than CSR for subsequent navigation?

SSR is slower for the first page load (server processing + network). Subsequent SPA navigation within the app is client-side and fast. The initial load tradeoff is worth it for SEO.

Does SSR work with Redux?

Yes. Create a Redux store on the server, dispatch actions, render the app, and serialize the store state into the HTML. Hydrate the store with the serialized state on the client.

Can I use SSR with code splitting?

Yes, but it requires careful configuration. Ensure the server knows which chunks to include in the HTML. Next.js and Nuxt handle this automatically.

Does SSR increase server costs?

Yes. The server must render pages for each request. Use caching (CDN, Redis) to reduce server load. Next.js ISR and incremental builds help manage costs.

Mini Project

Set up a Next.js application with 3 SSR pages (using getServerSideProps): a blog post page that fetches from an API, a user profile page with dynamic title, and a search page that reads query parameters. Verify that the server returns fully-rendered HTML by viewing the page source.

What's Next

You learned SSR for SPAs. Now explore pre-rendering — generating static HTML at build time for your SPA pages.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro