SSG, ISR, SSR, CSR: which strategy should I use in my Next.js e-commerce platform?

Introduction
In today's evolving web development landscape, selecting the right rendering strategy is vital for creating fast, scalable, and user-friendly applications. Next.js, a leading React framework, offers four powerful major rendering options: Static Site Generation (SSG), Incremental Static Regeneration (ISR), Server-Side Rendering (SSR), and Client-Side Rendering (CSR). Each approach comes with its own set of features and trade-offs.
For e-commerce platforms, where performance, SEO, and content freshness directly impact business success, choosing the optimal rendering method is critical. This choice can dramatically affect page load speeds, search engine visibility, and—by extension—user satisfaction and conversion rates.
With Next.js 13, the introduction of the App Router brings a new routing paradigm centered on React Server Components. This update offers enhanced flexibility and finer control over rendering strategies. While SSG, ISR, SSR, and CSR remain as core concepts, their implementation now relies primarily on caching behavior and component-level dynamic data fetching.
In this article, we explore how SSG, ISR, SSR, and CSR function in Next.js 13 and later versions. We'll examine their strengths and limitations to help you make informed decisions when building or improving e-commerce platforms.
1. Static Rendering vs Dynamic Rendering
In Next.js, page rendering depends on when and how HTML is generated: either at build time (static) or at request time (dynamic). With the App Router, Next.js automatically selects the optimal rendering strategy based on how components fetch and handle data.
By default, Next.js uses static rendering when possible to maximize performance and scalability. For pages requiring fresh or user-specific data, it switches to dynamic rendering.
⚙️ What triggers dynamic rendering?
Next.js switches to dynamic rendering in these major cases:
Condition | Effect |
---|---|
fetch() with cache: 'no-store' | Forces Server-Side Rendering (SSR) — no caching |
fetch() with next: { revalidate: 0 } | Also disables caching — triggers SSR |
export const dynamic = 'force-dynamic' | Explicitly instructs Next.js to use SSR |
Use of dynamic functions like cookies() , headers() | Makes page request-specific — disables static rendering |
Use params prop without generateStaticParams() and proper dynamicParams settings | generateStaticParams() allows pre-rendering of specific parameter values at build time, while remaining paths will be handled via SSR or return a 404, depending on the dynamicParams configuration. |
2. Static Site Generation (SSG)
Static Site Generation (SSG) generates pages at build time and serves them as static HTML files. This approach delivers exceptionally fast load times, high reliability, and excellent scalability because content is prepared before any user requests and can be efficiently distributed through CDNs.
✅ Key Advantages
- ⚡ Performance: Pages load instantly from CDN or edge cache.
- 🔒 Stability: No server-side rendering needed, minimizing potential failures.
- 🔍 SEO-ready: Complete HTML available immediately for search engines.
- 📈 Scalable: Perfect for high-traffic pages due to pre-rendered content.
⚠️ Limitations
- Content stays static between builds, updates requires additional application build.
- Not ideal for pages needing real-time updates or personalization.
Given these advantages, ideal candidates for SSG in e-commerce include:
- promotional or campaign landing pages,
- static marketing and brand content (e.g., about pages, policies, shipping info).
Product listing and details pages can also benefit from SSG when their content updates infrequently.
🛠️ Implementation
Let's examine how to implement a static product details page.
import "./ProductPage.css"; type Product = { id: string; name: string; description: string; }; const mockProducts: Product[] = [ { id: "1", name: "T-Shirt", description: "Comfortable cotton t-shirt." }, { id: "2", name: "Sneakers", description: "Lightweight running shoes." }, { id: "3", name: "Watch", description: "Elegant analog wristwatch." }, { id: "4", name: "Backpack", description: "Durable everyday backpack." }, { id: "5", name: "Sunglasses", description: "Polarized UV protection." }, { id: "6", name: "Hat", description: "Stylish summer hat." }, { id: "7", name: "Jacket", description: "Windproof outdoor jacket." }, { id: "8", name: "Jeans", description: "Slim-fit denim jeans." }, { id: "9", name: "Scarf", description: "Warm wool scarf." }, { id: "10", name: "Boots", description: "Waterproof hiking boots." }, ]; export const dynamicParams = false; export async function generateStaticParams() { return mockProducts.map((product) => ({ id: product.id, })); } export default async function ProductPage(props: { params: Promise<{ id: string }>; }) { const { id } = await props.params; const product = mockProducts.find((p) => p.id === id); // Since dynamicParams = false, this will never be rendered unless id is valid. return ( <main className="product-page"> <div className="product-card"> <h1 className="product-title">{product?.name}</h1> <p className="product-description">{product?.description}</p> <button className="product-button">Add to cart</button> </div> </main> ); }
app/product/ssg/[id]/page.tsx
🧠 Explanation
In the example above, we use Static Site Generation (SSG) to pre-render individual product pages at build time. Let's explore the key elements of the implementation:
generateStaticParams()
This asynchronous function tells Next.js which dynamic routes to pre-render. In our case, we hardcode 10 mock products, but it could be as well a call to Your database. Result would be the same: only these 10 product pages (e.g., /product/ssg/1
, /product/ssg/2
, ..., /product/ssg/10
) will be built statically.
server ├── app │ ├── _not-found │ └── favicon.ico │ └── product │ └── ssg │ └── [id] │ └── 1.html │ └── 2.html │ └── 3.html │ └── 4.html │ └── 5.html │ └── 6.html │ └── 7.html │ └── 8.html │ └── 9.html │ └── 10.html
dynamicParams = false
By setting this flag, we explicitly instruct Next.js to return a 404 error for any route that wasn't generated by generateStaticParams()
.
This means:
/product/ssg/3
→ ✅ built at build time/product/ssg/999
→ ❌ will automatically render the 404 page
Although we use the dynamic params
prop, pairing it with generateStaticParams()
enables complete SSG functionality — eliminating runtime rendering and the need for manual product validation in the component.
3. Incremental Static Regeneration (ISR)
What if our static product pages need frequent updates—daily or weekly? This is where ISR comes into play.
Incremental Static Regeneration (ISR) allows static pages to update in the background after deployment. It combines the performance benefits of Static Site Generation (SSG) with dynamic content updates—all without requiring a full rebuild.
For e-commerce sites where product data changes regularly—such as prices, availability, or descriptions—ISR offers an ideal solution when updates are needed more frequently than full deployments allow.
❓How it works
ISR generates pages at build time (like SSG), but includes a revalidation period. After this period expires, the next user request triggers a background regeneration. The new version is then cached and served to subsequent visitors.
✅ Key Advantages
- 🧠 Hybrid flexibility: Combines SSG benefits with content freshness.
- 🔁 On-demand freshness: Updates pages without full redeployment.
- 🚀 Performance first: Serves pre-rendered static content to most users.
- 🔧 Control via revalidation: Customisable content staleness thresholds.
- 🛠️ Programmatic control: Immediate page updates using
revalidatePath()
—perfect for admin panels or CMS triggers.
⚠️ Limitations
- Slightly higher complexity compared to pure SSG.
- Regeneration occurs after a stale page request.
- Revalidation cannot guarantee real-time consistency across all users.
🛠️ Implementation
Let's modify our previous product details page to implement ISR with hourly updates.
import { notFound } from "next/navigation"; import "./ProductPage.css"; type Product = { id: string; name: string; description: string; }; const mockProducts: Product[] = [ { id: "1", name: "T-Shirt", description: "Comfortable cotton t-shirt." }, { id: "2", name: "Sneakers", description: "Lightweight running shoes." }, { id: "3", name: "Watch", description: "Elegant analog wristwatch." }, { id: "4", name: "Backpack", description: "Durable everyday backpack." }, { id: "5", name: "Sunglasses", description: "Polarized UV protection." }, { id: "6", name: "Hat", description: "Stylish summer hat." }, { id: "7", name: "Jacket", description: "Windproof outdoor jacket." }, { id: "8", name: "Jeans", description: "Slim-fit denim jeans." }, { id: "9", name: "Scarf", description: "Warm wool scarf." }, { id: "10", name: "Boots", description: "Waterproof hiking boots." }, ]; export async function generateStaticParams() { return mockProducts.map((product) => ({ id: product.id, })); } // ISR configuration: revalidate every 1 hour export const revalidate = 3600; export default async function ProductPage(props: { params: Promise<{ id: string }>; }) { const { id } = await props.params; const product = mockProducts.find((p) => p.id === id); if (!product) { notFound(); } return ( <main className="product-page"> <div className="product-card"> <h1 className="product-title">{product.name}</h1> <p className="product-description">{product.description}</p> <button className="product-button">Add to cart</button> </div> </main> ); }
app/product/isr/[id]/page.tsx
export const revalidate = 3600
This line enables ISR with a 1-hour (3,600 seconds) revalidation period. During this time, pages are served from cache. Once the period expires, the next user request triggers a background page regeneration.
generateStaticParams()
As in the SSG example, we pre-generate pages for our 10 core products. These pages are built at build time, served as static HTML, and updated through ISR when needed.
Removed dynamicParams = false
Unlike our SSG example where we used dynamicParams = false
to limit access to statically generated routes, this constraint is now removed. This means Next.js will attempt to render any route matching the dynamic pattern (/product/isr/[id]
), regardless of whether it was included in generateStaticParams()
.
This enables us to:
- Serve product pages for new items without rebuilding the app.
- Dynamically generate pages for long-tail or user-generated content on demand.
- Return
404
conditionally when a product ID doesn't exist in the data source.
This flexibility makes the route perfect for hybrid/static-dynamic behavior—the essence of ISR.
4. Server Side Rendering (SSR)
So far, we've looked at Static Site Generation (SSG) — where pages are pre-built during deployment — and Incremental Static Regeneration (ISR), which adds scheduled or on-demand revalidation on top of SSG.
But what happens when your data changes constantly, depends on user authentication, or simply can't be predicted at build time?
Server-Side Rendering (SSR) is the answer.
With SSR, pages are rendered on the server at request time for each user visit. Rather than serving pre-rendered HTML from a cache, the server processes every incoming request individually, ensuring the data is always fresh.
✅ Key Advantages
- 🔄 Real-time data: Fresh content is fetched for each request, eliminating stale data.
- 👤 Personalization: Perfect for user-specific content like account pages and dashboards.
- 🔍 SEO optimization: Search engines receive fully rendered HTML with the latest content.
- 🔧 Dynamic control: Data fetching happens on demand — without rebuilds or revalidation.
⚠️ Limitations
- 🐢 Performance impact: Pages load more slowly than SSG/ISR since HTML must be generated for each request.
- 🧱 Scaling complexity: High traffic can strain server resources and increase response times.
- 💸 Higher costs: Increased server processing leads to greater infrastructure expenses.
🛠️ Implementation
Let's now modify our previous product details page to implement SSR, which will dynamically render the page each time an user requests it.
import { notFound } from "next/navigation"; import "./ProductPage.css"; type Product = { id: string; name: string; description: string; }; const mockProducts: Product[] = [ { id: "1", name: "T-Shirt", description: "Comfortable cotton t-shirt." }, { id: "2", name: "Sneakers", description: "Lightweight running shoes." }, { id: "3", name: "Watch", description: "Elegant analog wristwatch." }, { id: "4", name: "Backpack", description: "Durable everyday backpack." }, { id: "5", name: "Sunglasses", description: "Polarized UV protection." }, { id: "6", name: "Hat", description: "Stylish summer hat." }, { id: "7", name: "Jacket", description: "Windproof outdoor jacket." }, { id: "8", name: "Jeans", description: "Slim-fit denim jeans." }, { id: "9", name: "Scarf", description: "Warm wool scarf." }, { id: "10", name: "Boots", description: "Waterproof hiking boots." }, ]; export default async function ProductPage(props: { params: Promise<{ id: string }>; }) { const { id } = await props.params; const product = mockProducts.find((p) => p.id === id); if (!product) { notFound(); } return ( <main className="product-page"> <div className="product-card"> <h1 className="product-title">{product.name}</h1> <p className="product-description">{product.description}</p> <button className="product-button">Add to cart</button> </div> </main> ); }
app/product/ssr/[id]/page.tsx
🧠 Explanation
No generateStaticParams()
Unlike SSG and ISR, this implementation doesn’t use generateStaticParams()
at all. The route is fully dynamic — there's no list of valid product IDs generated at build time.
No dynamicParams
or revalidate
flags
Since this page is rendered on every request, there’s no need to configure dynamicParams
or revalidate
. Each time the page is accessed, Next.js will evaluate the component on the server.
5. Client-Side Rendering (CSR)
We've now covered Static Site Generation (SSG), Incremental Static Regeneration (ISR), and Server-Side Rendering (SSR)—all involving server-side logic or pre-rendering. But there's one more model to explore: Client-Side Rendering (CSR).
With CSR, the server sends a minimal HTML shell and JavaScript bundle to the browser. The browser then renders the page content entirely on the client by fetching data from an API or other source. This powers traditional Single Page Applications (SPAs) and remains a valid strategy in Next.js when used strategically.
While Client-Side Rendering is fundamental to React development, it's generally not ideal as a default rendering strategy at the page level in Next.js—particularly for content requiring strong SEO or fast initial visibility. Instead, CSR excels with specific, interactive components like filters, carousels, modals, or client-specific widgets.
✅ Key Advantages
- 🧠 Enhanced interactivity: Perfect for dynamic, app-like experiences where user interaction is crucial.
- 🧍 User-specific data: Content personalizes directly in the browser without server-side processing.
- 🚀 Seamless navigation: Next.js' built-in routing and client caching enable instant page transitions.
⚠️ Limitations
- 🐢 Slower initial load: Users must wait for JavaScript to load and data fetching to complete before seeing content.
- 🔍 SEO limitations: Search engines may struggle to index client-rendered content, despite improvements in modern crawlers.
- 🔌 JavaScript dependency: The page becomes non-functional if JavaScript is disabled or fails to load.
- 📦 Larger JS bundles: Additional client-side logic increases bundle size, affecting performance on slower devices and networks.
⚠️ Important Note
Even though Client Components run primarily in the browser, they are still pre-rendered on the server during the initial request. Next.js uses this approach to deliver a minimal HTML shell, which improves initial load performance and perceived speed.
After the HTML arrives, JavaScript "hydrates" the components on the client, making them fully interactive.
🧠 Be careful: When server-rendered markup doesn't match client-side JavaScript during hydration, you may encounter hydration errors. These errors can break your UI or trigger console warnings. Common causes include non-deterministic values (like Date.now()
, Math.random()
) or mismatched state between server and client.
🛠️ Implementation
"use client"; import { useParams } from "next/navigation"; import "../../ProductPage.css"; import { useEffect, useState } from "react"; type Product = { id: string; name: string; description: string; }; const mockProducts: Product[] = [ { id: "1", name: "T-Shirt", description: "Comfortable cotton t-shirt." }, { id: "2", name: "Sneakers", description: "Lightweight running shoes." }, { id: "3", name: "Watch", description: "Elegant analog wristwatch." }, { id: "4", name: "Backpack", description: "Durable everyday backpack." }, { id: "5", name: "Sunglasses", description: "Polarized UV protection." }, { id: "6", name: "Hat", description: "Stylish summer hat." }, { id: "7", name: "Jacket", description: "Windproof outdoor jacket." }, { id: "8", name: "Jeans", description: "Slim-fit denim jeans." }, { id: "9", name: "Scarf", description: "Warm wool scarf." }, { id: "10", name: "Boots", description: "Waterproof hiking boots." }, ]; export default function ProductPage() { const { id } = useParams(); const [product, setProduct] = useState<Product>(); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchProduct() { await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate network delay const product = await new Promise<Product | undefined>((resolve) => resolve(mockProducts.find((product) => product.id === id)) ); setProduct(product); setLoading(false); } fetchProduct(); }, [id]); if (loading) { return ( <main className="product-page"> <p className="loading-message">Loading product...</p> </main> ); } if (!product) { return ( <main className="product-page"> <p className="product-not-found">Product not found</p> </main> ); } return ( <main className="product-page"> <div className="product-card"> <h1 className="product-title">{product.name}</h1> <p className="product-description">{product.description}</p> <button className="product-button">Add to cart</button> </div> </main> ); }
app/product/csr/[id]/page.tsx
🧠 Explanation
All logic is inside useEffect()
Unlike SSG, ISR, or SSR where data fetching happens on the server, in CSR everything happens on the client, and all logic related to fetching product data is encapsulated inside a useEffect()
hook. This hook triggers after the component mounts and retrieves the necessary data based on the URL parameter.
**"use client"
directive**
This directive tells Next.js to treat the component as a client component, allowing hooks like useState, useEffect and useParams to function properly.
Route handling with useParams()
Instead of receiving params
from the server (like in SSG/SSR), the id
is pulled from the client-side router using useParams()
.
Custom loading state
As right now browser is not handling a loading state by itself (by loading the page content) there is a need to implement a custom one. To simulate real network latency setTimeout
delay has been used. This mimics how a real client would experience remote API requests.
🧾 6. Summary: Choosing the Right Rendering Strategy in Next.js for E-commerce
Next.js offers four major powerful rendering strategies—Static Site Generation (SSG), Incremental Static Regeneration (ISR), Server-Side Rendering (SSR) and Client-Side Rendering (CSR). Each strategy has unique benefits, and choosing the right one for each route or component lets you optimize for performance, SEO, interactivity, and scalability.
Here's how they compare:
Rendering strategy | Render Time | Best For | Pros | Cons |
---|---|---|---|---|
SSG | Build time | Static pages, rarely updated product details | 🔹 Fastest performance | |
🔹 Great for SEO 🔹 CDN-ready | 🔸 Requires rebuilds for updates | |||
🔸 Not suitable for live data | ||||
ISR | Build + Revalidate | Product pages updated periodically or on demand | 🔹 Balance of freshness & speed 🔹 Supports invalidation | 🔸 Slightly stale content possible |
🔸 More setup complexity | ||||
SSR | Per request | Dynamic content, personalization, stock/pricing | 🔹 Always up-to-date | |
🔹 Supports user-specific logic | 🔸 Slower first load 🔸 Requires server infrastructure | |||
CSR | Client-only | Isolated, interactive components (e.g. cart) | 🔹 Full interactivity 🔹 Useful for client-only logic | 🔸 SEO limitations 🔸 Slower initial render |
🔸 Larger JS bundle |
🛒 E-commerce Use Case Recommendations
- Landing pages, policies, info pages: → ✅ SSG
- Product listings/details with moderate updates: → ✅ ISR
- Product listings/details with frequent updates, personalized prices, live inventory, and A/B tests: → ✅ SSR
- Client-only interactions (cart, wishlist, filters): → ✅ CSR
🔭 7. What’s Next?
The rendering strategies discussed in this article — SSG, ISR, SSR, and CSR — form the foundation of Next.js, particularly for e-commerce applications where performance and SEO are crucial.
But this is just the beginning.
In upcoming articles, we'll explore more advanced techniques from recent Next.js versions, including:
- Streaming and Suspense for progressive rendering and better UX,
- Partial Prerendering (PPR) to efficiently combine static and dynamic elements on the same page,
- Techniques to optimize per-component rendering strategies in large-scale applications.
These approaches provide more precise control over performance and user experience—especially valuable for highly dynamic, personalized, and large-scale e-commerce platforms.
Stay tuned!