You spent three weeks building a beautiful React SPA. You deployed it. You Googled it. Google returns nothing. Or worse, it crawls your page and indexes <div id="root"></div>.
This isn't a bug. It's the fundamental tension between how React works and how search engines crawl the web. Client-side rendering hands the browser a blank HTML shell and says "figure it out." Googlebot sometimes does. Bing mostly doesn't. And "sometimes" is not an SEO strategy.
Server Side Rendering (SSR) in React solves this at the source, but the implementation details trip up a lot of developers. Let's fix that.
What Actually Happens During CSR vs. Server Side Rendering in React
Before writing a single line of code, it's worth being precise about the difference.
With Client-Side Rendering (CSR), the server sends this:
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
The browser downloads the JS bundle, executes React, and then renders your content. For a user on a fast connection, this feels fine. For a crawler with a 5-second timeout or a user on 3G, it's a blank page.
With Server Side Rendering (SSR) in React, the server runs your components and sends fully rendered HTML:
<!DOCTYPE html>
<html>
<head>
<title>My Product Page</title>
<meta name="description" content="Buy the best widget online" />
</head>
<body>
<div id="root">
<h1>Best Widget</h1>
<p>Buy the best widget online...</p>
</div>
<script src="/bundle.js"></script>
</body>
</html>
Crawlers get real content immediately. Then React "hydrates" the page on the client, attaching event listeners to the already-rendered HTML so it becomes interactive. This also directly improves your Core Web Vitals SEO scores, particularly LCP (Largest Contentful Paint), because meaningful content hits the browser much earlier.
How to Implement Server Side Rendering (SSR) in React With Express
You don't always need Next.js. Understanding raw SSR in React makes you a better developer regardless of what framework you ultimately use.
Step 1: Install dependencies
npm install react react-dom express
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-register
Step 2: Your React component (src/App.jsx):
import React from 'react';
export default function App({ title, description }) {
return (
<div>
<h1>{title}</h1>
<p>{description}</p>
</div>
);
}
Step 3: The SSR Express server (server.js):
require('@babel/register')({
presets: ['@babel/preset-env', '@babel/preset-react'],
});
const express = require('express');
const React = require('react');
const { renderToString } = require('react-dom/server');
const App = require('./src/App').default;
const app = express();
app.get('/', (req, res) => {
const props = {
title: 'Best Widget on the Market',
description: 'Buy the best widget online, fast shipping, great price.',
};
// Core of SSR in React: render component to HTML string on the server
const html = renderToString(React.createElement(App, props));
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="${props.description}" />
<title>${props.title}</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
app.listen(3000, () => console.log('SSR React server running on port 3000'));
Run it: node server.js. View source in your browser. You'll see fully rendered HTML, not a blank <div>.
Result: Crawlers index real content instantly. Time to First Contentful Paint drops. And you didn't install Next.js.
The Meta Tag Problem SSR in React Doesn't Automatically Solve
Here's the part most Server Side Rendering React tutorials skip: rendering HTML to the DOM isn't enough. Search engines also parse your <head>, including title, description, Open Graph tags, canonical URLs, and structured data. If those are hardcoded or missing, you're still leaving SEO ranking on the table.
Dynamically injecting per-route meta tags in SSR requires deliberate plumbing:
// Route-based meta config
const metaConfig = {
'/': {
title: 'Home | My Store',
description: 'The best widgets online.',
canonical: 'https://mystore.com/',
},
'/products': {
title: 'Products | My Store',
description: 'Browse our full product catalog.',
canonical: 'https://mystore.com/products',
},
};
app.get('*', (req, res) => {
const meta = metaConfig[req.path] || {
title: 'My Store',
description: 'Default description.',
canonical: `https://mystore.com${req.path}`,
};
const html = renderToString(React.createElement(App, {}));
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<title>${meta.title}</title>
<meta name="description" content="${meta.description}" />
<link rel="canonical" href="${meta.canonical}" />
<meta property="og:title" content="${meta.title}" />
<meta property="og:description" content="${meta.description}" />
</head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
This works, but it grows unwieldy fast across dozens of routes. This is where a dedicated SEO layer becomes valuable.
The power-seo npm package handles this cleanly. It generates structured, route-aware SEO tags as a middleware step that slots directly into your existing SSR React server. Define your SEO config once and it handles the injection. I've been using it on a project at ccbd.dev where managing meta tags per route was getting messy at scale. Worth evaluating if your route count is growing.
Hydration: The Final Piece of SSR in React
SSR in React gets your HTML to the client fast. But without hydration, your event handlers won't work. The browser displays the server-rendered HTML and just sits there.
Client entry point (src/index.jsx):
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
// Props must match exactly what the server rendered
const props = window.__INITIAL_PROPS__;
hydrateRoot(document.getElementById('root'), <App {...props} />);
Pass initial props from the SSR server to the client:
// Inside your server.js template string
res.send(`
...
<script>
window.__INITIAL_PROPS__ = ${JSON.stringify(props)};
</script>
<script src="/bundle.js"></script>
...
`);
hydrateRoot (React 18+) attaches event listeners to the existing server-rendered DOM without re-rendering it. The result: near-instant First Contentful Paint from SSR, plus full interactivity once the bundle loads. That's the complete Server Side Rendering in React pipeline. Server renders, client hydrates, user wins.
Key Takeaways
- SSR in React is just Node.js running your components before the HTTP response is sent. Once that clicks, everything else follows naturally.
-
Meta tags are a separate concern from rendering. Server Side Rendering (SSR) in React gets content into the DOM, but dynamic
<head>injection per route requires its own solution. - Core Web Vitals SEO is a real ranking factor. SSR's improvement to LCP and FID isn't just a performance win; it directly affects where you land in search results. Google has confirmed this.
-
Hydration mismatches will cost you hours. Server and client must render identically. Guard against
Date.now(),Math.random(), and browser-only APIs (window,document) running during server render. - Next.js is SSR in React with training wheels. Genuinely useful training wheels, but you'll debug it faster once you understand the raw mechanics underneath.
If you want to try this approach, here's the repo: https://github.com/CyberCraftBD/power-seo
Over to You
Are you using Server Side Rendering in React in production and what's the biggest pain point you've hit: hydration mismatches, meta tag management, or something else entirely?
Drop your setup in the comments. I'm especially curious whether teams are hand-rolling SSR or defaulting straight to Next.js, and whether the trade-offs were worth it for your use case.
Top comments (0)