20 min to read
Understanding Web Rendering Methods - CSR, SSR, SSG, and ISR
A comprehensive comparison of different web rendering approaches

Overview
Web rendering is the process of generating visual content from web code, including HTML, CSS, and JavaScript. Let’s explore the four main rendering approaches: CSR, SSR, SSG, and ISR.
The way web content is rendered significantly impacts user experience, performance, search engine optimization, and developer workflows. Modern web development has evolved beyond traditional server-side rendering to offer a spectrum of rendering strategies, each with distinct advantages and trade-offs. Understanding these approaches helps developers select the optimal rendering strategy for specific use cases.
Client-Side Rendering (CSR)
Client-Side Rendering shifts the rendering burden from servers to browsers, making it ideal for highly interactive, dynamic web applications.
Process:
- Browser downloads minimal HTML
- JavaScript loads and executes
- UI renders dynamically
- Handles user interactions
In client-side rendering, the server sends a minimal HTML file with links to JavaScript bundles. The browser downloads and executes these JavaScript files, which then:
- Initialize the JavaScript framework/library (React, Vue, Angular, etc.)
- Make API calls to fetch necessary data
- Generate DOM elements based on the received data
- Insert these elements into the page
- Attach event listeners for interactivity
This approach places most of the rendering logic in the client's browser.
Technical Implementation
A basic React CSR implementation looks like this:
Server response (minimal HTML):
<!DOCTYPE html>
<html>
<head>
<title>CSR App</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div id="root">
<!-- App will be rendered here -->
</div>
<!-- JavaScript bundles -->
<script src="/runtime.js"></script>
<script src="/vendor.js"></script>
<script src="/main.js"></script>
</body>
</html>
Client-side React code:
// App.jsx
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch data after component mounts
fetch('https://api.example.com/data')
.then(response => response.json())
.then(result => {
setData(result);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<div className="app">
<h1>{data.title}</h1>
<p>{data.description}</p>
{/* More UI elements */}
</div>
);
}
// Render the app when DOM is ready
ReactDOM.render(<App />, document.getElementById('root'));
Advantages
- Rich interactivity: Dynamic user interfaces with immediate feedback
- Reduced server load: Server only delivers static assets, not rendered pages
- Fast subsequent navigation: Only new data needs to be fetched, not entire pages
- Offline capabilities: Can work with service workers for offline functionality
- Development efficiency: Clean separation of frontend and backend
Disadvantages
- Slow initial load: Users must wait for JavaScript to download, parse, and execute
- Poor SEO performance: Search engines may not execute JavaScript properly
- JavaScript dependency: Requires JavaScript to be enabled in the browser
- Performance on low-end devices: Can be slow on less powerful devices
- Large bundle sizes: Can lead to longer download times
Server-Side Rendering (SSR)
Server-Side Rendering generates complete HTML pages on the server for each request, delivering a fully rendered page to the browser.
Process:
- Server processes request
- Fetches necessary data
- Generates complete HTML
- Sends rendered page
- Browser displays content
- Hydration adds interactivity
Hydration is a critical process in modern SSR applications that bridges server rendering and client interactivity. After the server sends pre-rendered HTML, the JavaScript bundle loads and:
- Reconnects event handlers to the existing DOM elements
- Sets up internal state to match the rendered output
- Makes the page interactive without re-rendering all content
Effective hydration is key to the performance benefits of SSR while maintaining interactivity.
Technical Implementation
A basic Next.js SSR implementation:
// pages/products/[id].js in Next.js
import React from 'react';
function ProductPage({ product }) {
return (
<div className="product">
<h1>{product.name}</h1>
<p className="price">${product.price}</p>
<p className="description">{product.description}</p>
<button onClick={() => console.log('Add to cart clicked')}>
Add to Cart
</button>
</div>
);
}
// This function runs on the server for every request
export async function getServerSideProps(context) {
const { id } = context.params;
// Fetch data from API or database
const response = await fetch(`https://api.example.com/products/${id}`);
const product = await response.json();
// Pass data to the page component as props
return {
props: { product }
};
}
export default ProductPage;
Advantages
- Fast initial page load: Pre-rendered HTML is displayed immediately
- SEO friendly: Search engines see the full content without executing JavaScript
- Consistent performance: Works well even on low-powered devices
- Better user experience: Users see meaningful content sooner
- Works without JavaScript: Basic content is visible even if JS fails or is disabled
Disadvantages
- Higher server load: Each request requires server processing
- Slower page transitions: Full page reloads between navigations
- More server resources needed: Rendering happens on the server
- Time to First Byte (TTFB): Can be slower as server needs to render the page
- Complex state management: Maintaining state between server and client can be challenging
(Content visible but not interactive) Browser->>Browser: Parse HTML and CSS Browser->>Server: Request JavaScript Server-->>Browser: Send JavaScript Browser->>Browser: Execute JavaScript
and Hydrate DOM Note over Browser: Time to Interactive Browser->>User: Fully Interactive Page
Static Site Generation (SSG)
Static Site Generation pre-builds all pages at build time rather than on-demand, resulting in extremely fast load times and efficient scaling.
Process:
- Pages built at build time
- Pre-renders all HTML/CSS/JS
- Serves via CDN
- Instant page loads
- Optional client-side hydration
Technical Implementation
A Gatsby (React-based SSG) implementation:
// src/templates/product.js in Gatsby
import React from 'react';
import { graphql } from 'gatsby';
function ProductTemplate({ data }) {
const product = data.productsApi.product;
return (
<div className="product">
<h1>{product.name}</h1>
<p className="price">${product.price}</p>
<p className="description">{product.description}</p>
<button onClick={() => console.log('Add to cart clicked')}>
Add to Cart
</button>
</div>
);
}
// This query runs at build time
export const query = graphql`
query ProductPage($id: String!) {
productsApi {
product(id: $id) {
id
name
price
description
}
}
}
`;
export default ProductTemplate;
Build process configuration (gatsby-node.js):
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
// Query all products at build time
const result = await graphql(`
{
productsApi {
products {
id
slug
}
}
}
`);
// Create a page for each product
result.data.productsApi.products.forEach(product => {
createPage({
path: `/product/${product.slug}`,
component: require.resolve('./src/templates/product.js'),
context: {
id: product.id
}
});
});
};
Advantages
- Extremely fast loading: Pre-built HTML served directly from CDN
- Great SEO: Complete HTML content available for search engines
- Highly scalable: Static files are easy and cheap to distribute globally
- Low server costs: No server-side rendering required for each request
- Enhanced security: Reduced attack surface with no server processing
- Reliability: Less prone to runtime errors
Disadvantages
- Long build times: Can be slow to build sites with many pages
- Limited dynamic content: Content is fixed at build time
- Requires full rebuild for updates: Changes require rebuilding affected pages
- Not suitable for all content: Frequently changing data is challenging
- Build complexity: Large sites require optimized build processes
Incremental Static Regeneration (ISR)
Incremental Static Regeneration combines the benefits of static generation with the ability to update content without rebuilding the entire site.
Process:
- Initial static generation
- Serve cached pages
- Background revalidation
- Selective page updates
- Update the cache
ISR offers multiple approaches to updating content:
Time-based Revalidation:- Pages are regenerated after a specified time period
- Useful for content that changes on a predictable schedule
- Configured with a revalidation window (e.g., every hour)
- Pages are regenerated when explicitly triggered
- Useful for content managed through a CMS
- Can be triggered by webhooks or API calls
Both strategies serve stale content while regenerating in the background, ensuring uninterrupted user experience.
Technical Implementation
A Next.js ISR implementation:
// pages/posts/[slug].js in Next.js
import React from 'react';
function Post({ post, lastUpdated }) {
return (
<div className="post">
<h1>{post.title}</h1>
<p className="metadata">Last updated: {new Date(lastUpdated).toLocaleString()}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
);
}
export async function getStaticProps({ params }) {
const { slug } = params;
// Fetch data for this specific post
const response = await fetch(`https://api.example.com/posts/${slug}`);
const post = await response.json();
return {
props: {
post,
lastUpdated: Date.now()
},
// Revalidate the page every 60 seconds
revalidate: 60
};
}
export async function getStaticPaths() {
// Fetch the most important posts at build time
const response = await fetch('https://api.example.com/posts/popular');
const popularPosts = await response.json();
// Pre-render only popular posts
const paths = popularPosts.map(post => ({
params: { slug: post.slug }
}));
return {
paths,
// Generate other pages on-demand
fallback: 'blocking'
};
}
export default Post;
On-demand revalidation API route (Next.js):
// pages/api/revalidate.js
export default async function handler(req, res) {
// Check for secret to confirm this is a valid request
if (req.query.secret !== process.env.REVALIDATION_TOKEN) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
// Extract slug from request body
const { slug } = req.body;
// Revalidate the specific post page
await res.revalidate(`/posts/${slug}`);
return res.json({ revalidated: true });
} catch (err) {
// If there was an error, Next.js will continue to show the last successfully generated page
return res.status(500).send('Error revalidating');
}
}
Advantages
- Dynamic content updates: Content can be updated without full rebuilds
- Fast initial loads: Serves pre-generated static pages
- Scalable architecture: Most requests serve cached content
- Reduced build times: Only build critical pages at deployment
- Background regeneration: Updates happen without affecting user experience
- Selective rebuilds: Only changed pages need to be regenerated
Disadvantages
- Implementation complexity: More complex setup than pure SSG
- Potential stale content: Users might see outdated content during revalidation
- Cache management needed: Requires consideration of caching strategies
- More complex deployment: Needs hosting that supports ISR
- Fallback considerations: Must handle fallback states appropriately
Performance Impact
Each rendering method has different impacts on key performance metrics:
Metric | CSR | SSR | SSG | ISR |
---|---|---|---|---|
Time to First Byte (TTFB) | Fast | Slow | Fastest | Fast |
First Contentful Paint (FCP) | Slow | Medium | Fast | Fast |
Largest Contentful Paint (LCP) | Slow | Medium | Fast | Fast |
Time to Interactive (TTI) | Slow | Medium | Fast | Fast |
Total Blocking Time (TBT) | High | Medium | Low | Low |
Cumulative Layout Shift (CLS) | High | Medium | Low | Low |
Rendering Methods Comparison
Feature | CSR | SSR | SSG | ISR |
---|---|---|---|---|
Initial Load | Slow | Fast | Fastest | Fast |
SEO | Poor | Good | Excellent | Good |
Dynamic Content | Excellent | Good | Poor | Good |
Server Load | Low | High | Lowest | Medium |
Build Time | N/A | N/A | Long | Medium |
Developer Experience | Simple | Complex | Medium | Complex |
Infrastructure Requirements | Static hosting | Node.js server | Static hosting | Specialized hosting |
Data Freshness | Real-time | Real-time | Static | Periodically updated |
Use Cases and Implementations
- Use Cases: SPAs, dashboards, admin interfaces, highly interactive applications
- Tools/Frameworks: React, Angular, Vue.js, Svelte
- Implementation Example: Create React App, Vue CLI projects
- Security Considerations: Exposed API keys risk, CORS issues, XSS vulnerabilities
- Real-world Examples: Gmail, Facebook, Twitter web app
- Use Cases: News websites, e-commerce product pages, content-heavy sites, SEO-critical pages
- Tools/Frameworks: Next.js, Nuxt.js, Angular Universal, Remix
- Implementation Example: Next.js with getServerSideProps
- Security Considerations: Better control over data flow, server exposure, CSRF protection needed
- Real-world Examples: The New York Times, Amazon product pages
- Use Cases: Blogs, documentation sites, marketing websites, portfolio sites
- Tools/Frameworks: Gatsby, Next.js, Hugo, Jekyll, 11ty
- Implementation Example: Next.js with getStaticProps, Gatsby
- Security Considerations: Secure with minimal server interaction, reduces attack vectors
- Real-world Examples: React documentation, Stripe documentation
- Use Cases: E-commerce product pages with frequent updates, blogs with comments, news with moderate update frequency
- Tools/Frameworks: Next.js (ISR), Nuxt 3, Gatsby (DSG)
- Implementation Example: Next.js with revalidate option in getStaticProps
- Security Considerations: Cache invalidation risks, revalidation endpoint security
- Real-world Examples: Shopify storefronts built with Hydrogen
Hybrid Approaches
Modern web applications often combine multiple rendering strategies for optimal results:
Next.js App Router
Next.js 13+ introduces a hybrid rendering model with granular control:
// Server Component (SSR by default)
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Client Component for interactive elements */}
<AddToCartButton productId={product.id} />
</div>
);
}
// Client Component for interactivity
'use client'
import { useState } from 'react';
export function AddToCartButton({ productId }) {
const [isAdding, setIsAdding] = useState(false);
async function handleAddToCart() {
setIsAdding(true);
await addToCart(productId);
setIsAdding(false);
}
return (
<button
onClick={handleAddToCart}
disabled={isAdding}
>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
);
}
Islands Architecture
Partial hydration with interactive islands in an otherwise static page:
<!-- Static HTML from SSG -->
<header>
<h1>Product Catalog</h1>
</header>
<main>
<div class="product-grid">
<!-- Static product cards -->
</div>
<!-- Interactive shopping cart island -->
<div
data-island="shopping-cart"
data-props='{"initialItems": []}'
>
<p>Loading cart...</p>
</div>
</main>
<script type="module">
// Only hydrate specific islands
import { ShoppingCart } from '/components/shopping-cart.js';
// Find all islands
document.querySelectorAll('[data-island="shopping-cart"]').forEach(island => {
const props = JSON.parse(island.dataset.props);
// Hydrate just this component
render(ShoppingCart, props, island);
});
</script>
Making the Right Choice
When selecting a rendering approach, consider these factors:
- Content Type: Is your content static, dynamic, or a mix?
- Update Frequency: How often does your content change?
- SEO Importance: How critical is search engine visibility?
- User Interaction: How interactive is your application?
- Performance Requirements: What are your performance targets?
- Development Resources: What technical expertise is available?
- Infrastructure Constraints: What hosting options do you have?
Start by answering these questions:
- Is SEO critical? If yes, avoid pure CSR
- Is content highly dynamic (changes with each user)? If yes, use SSR or CSR
- Is content updated frequently but the same for all users? Consider ISR
- Is the site mostly static with infrequent updates? SSG is ideal
- Do you need maximum interactivity? Use CSR or a hybrid approach
- Are server resources limited? Prefer SSG or ISR over SSR
The best solution is often a hybrid approach, using different rendering strategies for different parts of your application.
Comments