
Introduction
Server-Side Rendering (SSR) is a technique that enables a React application to be rendered on the server rather than in the client’s browser. This approach offers several benefits over traditional client-side rendering, including improved performance, better SEO, and enhanced user experience. In this chapter, we’ll explore how SSR works in React and how to implement it in your applications.
Understanding Server Side Rendering
What is Server-Side Rendering?
Server-Side Rendering refers to the process of converting React components into HTML on the server before sending them to the client’s browser. With traditional client-side rendering, the browser receives minimal HTML and a bundle of JavaScript that needs to be executed before users can see the content. In contrast, SSR delivers fully rendered HTML to the browser, allowing users to see content immediately while JavaScript is still loading.
Client-Side Rendering vs. Server-Side Rendering
Client-Side Rendering | Server-Side Rendering |
Initial rendering happens in the browser | Initial rendering happens on the server |
Slower initial load | Faster initial content display |
Poorer SEO performance | Better SEO performance |
Lower server load | Higher server load |
Single-page application experience | Can still function as an SPA after initial load |
Why Use Server-Side Rendering?
- Performance: Faster initial page load and Time to First Contentful Paint (FCP)
- SEO: Search engines can crawl the fully rendered content
- Social Media Sharing: Proper metadata for sharing on platforms like Facebook, Twitter, etc.
- Accessibility: Content is available even if JavaScript fails to load or execute
- User Experience: Users see content immediately rather than a loading spinner
Server-Side Rendering Frameworks for React
While you can implement SSR from scratch, several frameworks simplify the process:
Next.js
Next.js is the most popular React framework for SSR. It provides a powerful yet easy-to-use approach to building React applications with server-side rendering.
Key features:
- Automatic server rendering
- Static site generation (SSG) option
- API routes
- File-based routing
- CSS/Sass support with built-in styling options
- Code splitting
- Hot module replacement during development
Remix
Remix is a newer full-stack React framework focusing on web standards and modern UX patterns.
Key features:
- Nested routes
- Loaders and actions for data handling
- Error boundaries at the route level
- Progressive enhancement
- Optimistic UI updates
Gatsby
Gatsby primarily focuses on static site generation but also offers SSR capabilities.
Key features:
- Extensive plugin ecosystem
- GraphQL data layer
- Image optimization
- Performance optimizations built-in
Implementing SSR with Next.js
Since Next.js is the most widely adopted SSR solution for React, we’ll focus on implementing SSR using this framework.
Setting Up a Next.js Project
# Create a new Next.js project
npx create-next-app@latest my-ssr-app
cd my-ssr-app
# Start the development server
npm run dev
Creating Server-Rendered Pages
In Next.js, pages are automatically server-rendered by default. Create a page by adding a React component file to the pages directory:
// pages/index.js
export default function HomePage() {
 return (
  <div>
   <h1>Hello from the server!</h1>
   <p>This page was rendered on the server.</p>
  </div>
 );
}
Data Fetching Methods
Next.js provides several data fetching methods for fetching data on the server:
1. getServerSideProps
Use getServerSideProps when you need to fetch data on every request:
// pages/products.js
export default function Products({ products }) {
return (
<div>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
</div>
);
}
// This runs on the server for every request
export async function getServerSideProps() {
// Fetch data from an API
const res = await fetch('https://api.example.com/products');
const products = await res.json();
// Pass data to the page via props
return {
props: {
products,
},
};
}
2. getStaticProps
Use getStaticProps when data can be fetched at build time:
// pages/blog.js
export default function Blog({ posts }) {
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}
// This runs at build time
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: {
posts,
},
// Re-generate at most once per hour
revalidate: 3600,
};
}
3. getStaticPaths
Use getStaticPaths with getStaticProps for dynamic routes:
// pages/posts/[id].js
export default function Post({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Specify which paths to pre-render
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
// Get the paths we want to pre-render
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}));
return {
paths,
fallback: 'blocking', // or true or false
};
}
// Fetch data for each post
export async function getStaticProps({ params }) {
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
return {
props: {
post,
},
revalidate: 3600,
};
}
Hydration
After the server sends HTML to the client, React needs to “hydrate” the application, which means attaching event listeners and making the page interactive. Next.js handles this automatically.
The process works as follows:
- Server renders React components to HTML
- HTML is sent to the client with a reference to the JavaScript bundle
- Client downloads and executes the JavaScript
- React “hydrates” the HTML, preserving the server-rendered content while adding interactivity
Common SSR Challenges and Solutions
1. Browser-Specific APIs
Code that uses browser-specific APIs (like window or document) will fail during server rendering.
Solution: Use conditional checks or useEffect
import { useEffect, useState } from 'react';
function WindowSize() {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
// This code only runs on the client
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
handleResize(); // Call once to set initial size
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div>
{windowSize.width ? (
<p>Window size: {windowSize.width}px x {windowSize.height}px</p>
) : (
<p>Loading window size...</p>
)}
</div>
);
}
2. State Management with SSR
When using Redux or other state management libraries with SSR, you need to:
- Create a new store instance for each request
- Initialize the store with data from the server
- Serialize the store state and send it to the client
Example with Next.js and Redux:
// lib/store.js
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
export function initializeStore(preloadedState = {}) {
return configureStore({
reducer: rootReducer,
preloadedState,
});
}
// pages/_app.js
import { Provider } from 'react-redux';
import { initializeStore } from '../lib/store';
import { useStore } from '../lib/useStore';
export default function MyApp({ Component, pageProps }) {
const store = useStore(pageProps.initialReduxState);
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
3. CSS in SSR
Styling can be tricky with SSR. Solutions include:
- CSS Modules: Works well with Next.js out of the box
- Styled Components: Requires special setup for SSR
- Emotion: Similar to Styled Components
- Tailwind CSS: Works well with SSR
Example with styled-components in Next.js:
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
Universal (Isomorphic) JavaScript
Universal or Isomorphic JavaScript refers to code that can run both on the server and in the browser. This is a key concept in SSR applications.
Writing Universal Code
When writing universal code:
- Avoid direct access to browser or Node.js specific APIs
- Use environment checks when necessary
- Use abstractions that work in both environments
// An isomorphic fetch function
import nodeFetch from 'node-fetch';
const isomorphicFetch = async (url, options) => {
if (typeof window !== 'undefined') {
// Browser environment
return window.fetch(url, options);
} else {
// Node.js environment
return nodeFetch(url, options);
}
};
export default isomorphicFetch;
Performance Optimization for SSR
1. Code Splitting
Even with SSR, it’s important to minimize JavaScript bundle size:
// Using dynamic imports in Next.js
import dynamic from 'next/dynamic';
// Component is loaded dynamically only when needed
const DynamicChart = dynamic(() => import('../components/Chart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // Disable SSR for this component
});
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<DynamicChart />
</div>
);
}
2. Caching
Implement caching strategies to reduce server load:
- Use CDNs for static assets
- Implement cache headers
- Consider Redis or Memcached for data caching
- Use Incremental Static Regeneration (ISR) in Next.js
3. Streaming SSR
Newer versions of React and frameworks like Next.js 13+ support streaming SSR, which allows sending HTML in chunks as it’s generated:
// Next.js 13+ app directory approach
// app/page.js
import { Suspense } from 'react';
import SlowComponent from '../components/SlowComponent';
export default function Page() {
  return (
  <div>
   <h1>Welcome to my app</h1>
  Â
   {/* This content will be shown immediately */}
   <p>This content is available right away</p>
  Â
   {/* SlowComponent will stream in when ready */}
   <Suspense fallback={<p>Loading slow component...</p>}>
    <SlowComponent />
   </Suspense>
  </div>
 );
}
SSR in Production
Deployment Considerations
When deploying SSR applications:
- Node.js Server: You need a Node.js environment to run the server-side code
- Memory Requirements: SSR uses more memory than static sites
- Scaling: Consider horizontal scaling for high-traffic applications
- Containerization: Docker can be useful for consistent deployments
Deployment Platforms
Several platforms make deploying SSR React applications easy:
- Vercel: Optimized for Next.js deployments
- Netlify: Supports SSR via serverless functions
- AWS Amplify: Managed hosting with SSR support
- DigitalOcean App Platform: Simple deployment for SSR apps
- Heroku: Traditional platform with good Node.js support
- Self-hosted: Using PM2, Nginx, etc.
When Not to Use SSR
While SSR has many benefits, it’s not always the best choice:
- Admin Dashboards: Internal tools often don’t need SEO
- Simple Applications: The added complexity may not be worth it
- API-Only Use Cases: When your app is consumed only via API
- Limited Server Resources: SSR increases server load
In these cases, consider client-side rendering or static site generation instead.
Hybrid Approaches
Modern frameworks support hybrid approaches that combine the benefits of multiple rendering strategies:
- Static Site Generation (SSG): Pre-render pages at build time
- Incremental Static Regeneration (ISR): Update static pages in the background
- Client-Side Rendering (CSR): Render non-critical pages on the client
- Server-Side Rendering (SSR): Use for pages that need fresh data
Next.js allows mixing these approaches in a single application:
// Static page with ISR
// pages/products/index.js
export async function getStaticProps() {
// ...fetch data
return {
props: { products },
revalidate: 60, // Update at most every minute
};
}
// Server-rendered page
// pages/profile.js
export async function getServerSideProps({ req }) {
// Get user from session cookie
const user = getUserFromCookie(req);
return { props: { user } };
}
// Client-rendered component
// components/RealTimeData.js
function RealTimeData() {
const [data, setData] = useState(null);
useEffect(() => {
// Fetch on the client only
fetchRealTimeData().then(setData);
}, []);
return data ? <div>{data}</div> : <div>Loading...</div>;
}
Summary
Server-Side Rendering offers significant benefits for React applications, particularly in terms of performance, SEO, and user experience. Modern frameworks like Next.js make implementing SSR straightforward, providing powerful features that help you build robust server-rendered applications.
As you implement SSR in your projects, remember to consider:
- The right data fetching strategy for each page
- How to handle browser-specific code
- Performance optimization techniques
- Whether a hybrid approach might be best
In the next chapter, we’ll explore the React Context API, which provides a way to share data across components without prop drilling.