CSR vs SSR in Next.js: From Basics to Advanced Implementation
Introduction
When building modern web applications with Next.js, one of the most crucial decisions you'll make is choosing the right rendering strategy. Client-Side Rendering (CSR) and Server-Side Rendering (SSR) each have their strengths and use cases. This guide will walk you through everything you need to know about CSR vs SSR in Next.js.
What is Rendering?
Rendering is the process of converting your React components into HTML that can be displayed in the browser.
Next.js supports multiple rendering methods:
- CSR (Client-Side Rendering)
- SSR (Server-Side Rendering)
- SSG (Static Site Generation)
- ISR (Incremental Static Regeneration)
But let’s focus on CSR and SSR.
What is CSR?
Client-Side Rendering (CSR) is a rendering approach where the initial HTML sent to the browser is minimal, and JavaScript running in the browser generates the content dynamically.
How CSR Works:
What is SSR?
Server-Side Rendering (SSR) is a rendering approach where the HTML is generated on the server for each request, then sent to the browser as a fully-formed page.
How SSR Works:
Key Differences
Aspect | CSR | SSR |
---|---|---|
Initial Load | Slower (JavaScript must load first) | Faster (HTML ready immediately) |
SEO | Poor (content loaded via JS) | Excellent (content in HTML) |
Server Load | Lower | Higher |
Subsequent Navigation | Faster | Slower |
User Experience | Blank page → Content appears | Content appears immediately |
Caching | Easier to cache assets | More complex caching |
Code Examples
Client-Side Rendering Example
// pages/csr-example.js
import { useState, useEffect } from 'react';
function CSRExample() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This runs only on the client
fetch('/api/data')
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>Client-Side Rendered Page</h1>
<p>Data: {data?.message}</p>
</div>
);
}
export default CSRExample;
Server-Side Rendering Example
// pages/ssr-example.js
function SSRExample({ data }) {
return (
<div>
<h1>Server-Side Rendered Page</h1>
<p>Data: {data.message}</p>
<p>Generated at: {data.timestamp}</p>
</div>
);
}
// This function runs on the server for each request
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return {
props: {
data: {
...data,
timestamp: new Date().toISOString()
}
}
};
}
export default SSRExample;
Performance Comparison
Time to First Byte (TTFB)
- CSR: Fast TTFB (but slower content rendering)
- SSR: Slower (server processing time)
First Contentful Paint (FCP)
- CSR: Slow (wait for JS execution)
- SSR: Fast (HTML ready immediately)
Time to Interactive (TTI)
- CSR: Slow (dependent on JS bundle size)
- SSR: Moderate (hydration required)
Performance Optimization Example
// pages/optimized-ssr.js
import dynamic from 'next/dynamic';
// Lazy load heavy components
// ! `ssr: false` turns the component into a client component.
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false // Disable SSR for this component
});
function OptimizedSSR({ criticalData }) {
return (
<div>
<h1>Optimized SSR Page</h1>
<div>Critical content: {criticalData.message}</div>
{/* This component loads only on client */}
<HeavyComponent />
</div>
);
}
export async function getServerSideProps() {
// Only fetch critical data on server
const res = await fetch('https://api.example.com/critical-data');
const criticalData = await res.json();
return {
props: {
criticalData
}
};
}
export default OptimizedSSR;
When to Use What
Use CSR When:
- Building a dashboard or admin panel
- User interaction is frequent
- SEO is not a priority
- You need real-time updates
- Building a Single Page Application (SPA)
Use SSR When:
- SEO is crucial
- First-page load performance matters
- Content changes frequently
- Building e-commerce sites
- Social media sharing is important
Advanced Topics
Dynamic Imports for Code Splitting
// Advanced code splitting example
import dynamic from 'next/dynamic';
import { useState } from 'react';
const DynamicChart = dynamic(() => import('../components/Chart'), {
loading: () => <p>Loading chart...</p>,
ssr: false
});
const DynamicTable = dynamic(() => import('../components/Table'), {
loading: () => <p>Loading table...</p>
});
function AdvancedCSR() {
const [view, setView] = useState('chart');
return (
<div>
<nav>
<button onClick={() => setView('chart')}>Chart View</button>
<button onClick={() => setView('table')}>Table View</button>
</nav>
{view === 'chart' && <DynamicChart />}
{view === 'table' && <DynamicTable />}
</div>
);
}
Streaming SSR (React 18+)
// pages/streaming-ssr.js
import { Suspense } from 'react';
function SlowComponent() {
// Simulate slow component
await new Promise((res) => setTimeout(res, 3000));
return <div>Slow loading content</div>;
}
function StreamingSSR() {
return (
<div>
<h1>Streaming SSR Example</h1>
<p>This content loads immediately</p>
<Suspense fallback={<div>Loading slow content...</div>}>
<SlowComponent />
</Suspense>
</div>
);
}
export default StreamingSSR;
Edge-Side Rendering with Middleware
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
const country = request.geo?.country || 'US';
// Customize response based on location
const response = NextResponse.next();
response.headers.set('x-country', country);
return response;
}
export const config = {
matcher: '/api/:path*'
};
// Note: Middleware runs before requests but cannot render HTML. it's useful for things like redirects, geolocation, or A/B testing logic.
Best Practices
1. Choose the Right Rendering Strategy
// Decision matrix in code
const getRenderingStrategy = (pageType, requirements) => {
if (requirements.seo && requirements.freshData) {
return 'SSR';
}
if (requirements.interactivity && !requirements.seo) {
return 'CSR';
}
return 'Hybrid';
};
2. Optimize Bundle Size
// next.config.js
module.exports = {
experimental: {
css: true,
},
webpack: (config) => {
config.optimization.splitChunks.chunks = 'all';
return config;
}
};
3. Implement Progressive Enhancement
// Progressive enhancement example
function ProgressiveForm() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return (
<form>
<input type="text" name="name" required />
<input type="email" name="email" required />
{/* Enhanced features only on client */}
{isClient && (
<div>
<AutoComplete />
<RealTimeValidation />
</div>
)}
<button type="submit">Submit</button>
</form>
);
}
4. Error Handling and Loading States
// Comprehensive error handling
function RobustSSR({ data, error }) {
if (error) {
return <ErrorComponent error={error} />;
}
return (
<div>
<h1>Robust SSR Page</h1>
<p>{data?.message}</p>
</div>
);
}
export async function getServerSideProps() {
try {
const res = await fetch('https://api.example.com/data');
if (!res.ok) {
throw new Error('Failed to fetch data');
}
const data = await res.json();
return {
props: { data }
};
} catch (error) {
return {
props: {
error: error.message
}
};
}
}
Conclusion
Choosing between CSR and SSR in Next.js isn't about picking one over the other, it's about understanding your application's requirements and users needs. Here's a quick recap:
- Use SSR for SEO-critical pages and better initial load performance
- Use CSR for highly interactive applications where SEO isn't a priority
Next.js makes it easy to implement any of these strategies, and you can even mix them within the same application. The key is to profile your application, understand your users' journey, and choose the rendering strategy that provides the best user experience.
Remember: Performance is not just about fast loading, it's about the right content being available at the right time for your users.
📚 Further Reading: Server and Client Components
Top comments (1)
thanks for shared