Rendering methods in Next.js
Modern Next.js (App Router) gives you multiple rendering strategies. Understanding when to use each one is critical for performance, scalability, and user experience.
SSG: Static Site Generation
SSG generates your page HTML at build time. These pages are served instantly via a CDN, making them extremely fast.
Whenever we use the app router in next.js then all server components are static by default if they are not using any params means if your component is not doing anything dynamic then it is already static generated
if we have to generate static pages for dynamic routes e.g. - blogs. currently you are viewing the blog is dynamic page which is generated static at build time using generateStaticParams
it use slug to fetch and generate page at build time
1// app/page.tsx2// This is SSG by default — no special config needed.34export default function HomePage() {5 return (6 <main>7 <h1>Hey</h1>8 <p>We build tools for developers who value their time.</p>9 </main>10 );11}12
For dynamic routes like /blog/[slug], we use generateStaticParams.
This route is dynamic, but it can still be statically generated at build time.
1export async function generateStaticParams() {2 const payload = await getPayload({ config: configPromise });34 const blogs = await payload.find({5 collection: "blogs",6 limit: 1000,7 select: {8 slug: true, // each will have a dynamic path9 },10 });1112 return blogs.docs13 .map(blog => blog.slug)14 .filter((slug): slug is string => typeof slug === 'string')15 .map(slug => ({ slug }));16}17
so above code tells that we need to generate this amount of static pages one per slug at build time and those served via CDN instantly
so SSG should only use where page is mostly static or change less frequently and it should not use where we will need real time updates or dynamic pages and if your data comes from CMS and you can trigger a rebuild on publish, then SSG covers more pages than you think it can literally generate hundreds or more than that pages statically at build time
SSR: server side rendering
SSR is used when we need fresh data every time and it generates html on every time we send request and for every request we call API and every API call hit to DB or your server
1// app/contacts/page.tsx2// Fresh data on every request — no caching.34async function getContacts(session) {5 const res = await fetch(`https://api.example.com/contacts`, {6 cache: "no-store", // force fresh fetch every request7 });8 return res.json();9}1011export default async function ContactsPage() {12 const session = await getSession();13 const data = await getContacts(session);1415 return (16 <main>17 <p>Name: {data.name}</p>18 <p>Email: {data.email}</p>19 <p>Company: {data.company}</p>20 </main>21 );22}23
So SSR should only be use when we need real time updated data and many times people always use SSR and they even don’t see that am i ok with 5 mins old data or not (in that case ISR comes in picture) and this is very expensive at scale because at every user req you are calling DB or API and when there is lot of users it will make serious situation
ISR: Incremental Static Regeneration
ISR is use where we know like i will need new dynamic data but not instant i am ok with some amount of delay e.g. - 60 sec, 5 min, 1 hr
so it is statically generated page but it provides expiry parameter that after particular time it will regenerate the page and store and served as a cached
1// app/products/page.tsx2// Static page that revalidates every 60 seconds.34async function getProducts() {5 const res = await fetch("<https://api.example.com/products>", {6 next: { revalidate: 3600 }, // regenerate after 1 hour7 });8 return res.json();9}1011export default async function ProductsPage() {12 const products = await getProducts();1314 return (15 <ul>16 {products.map((product) => (17 <li key={product.id}>18 {product.name} — ${product.price}19 </li>20 ))}21 </ul>22 );23}
we can set custom revalidate logic based on backend or CMS let’s someone change the product price then we have set revalidate route that will regenerate the page if some one change the product details so cache will only cleared when some values is changed
so ISR is used when data is changed but no need to show in real time
CSR: client side rendering
it is used when in a page we have are doing data polling or state management so in next every time we do “use client”; it make that page or component as rendered on client side that shows loading at first or blank screen then after that it shows other elements
so initially minimal html page is being loaded and then after data is fetched using hooks or lib like SWR or TanStack query
1"use client";2import React, { useState, useEffect } from 'react'34export function Page() {5 const [data, setData] = useState(null)67 useEffect(() => {8 const fetchData = async () => {9 const response = await fetch('<https://api.example.com/data>')10 if (!response.ok) {11 throw new Error(`HTTP error! status: ${response.status}`)12 }13 const result = await response.json()14 setData(result)15 }1617 fetchData().catch((e) => {18 // handle the error as needed19 console.error('An error occurred while fetching the data: ', e)20 })21 }, [])2223 return <p>{data ? `Your data: ${data}` : 'Loading...'}</p>24}
any time we have to use API and state management then only we can use CSR other without need it can affect the performance due to unnecessary js
PPR: partial prerendering
it is rendering strategy that combines both static and dynamic rendering and it provide good performance for static site with dynamic parts
At build time static html is generated and for dynamic we wrap that part in react suspense so while user is seeing static content and it will fetch dynamic content and display that
1import { getPayload } from "payload";2import config from "@payload-config";34export default async function ProductPage({5 params,6}: {7 params: Promise<{ slug: string }>;8}) {9 const { slug } = await params;10 const payload = await getPayload({ config });1112 const result = await payload.find({13 collection: "products",14 where: { slug: { equals: slug } },15 limit: 1,16 depth: 2,17 });1819 const product = result.docs[0];20 if (!product) notFound();2122 return (23 <>24 <div>25 <p>{product.title}</p>26 // other product details27 </div>28 <div>29 <Suspense fallback={<p>Loading reviews...</p>}>30 <Reviews productId={product.id} /> /* this will fetch product reviews using api or other31 lib and this will be dynamic */32 </Suspense>33 </div>34 </>35 )36};
if you want to use you need to test everything first because if there is page with everything wrap in suspense then you can use SSR and you need to think that this is need or not
Important Configs
1revalidate = false // fully static (SSG)2revalidate = 0 // always dynamic (SSR)3revalidate = 60 // ISR45dynamic = "force-static" // force SSG6dynamic = "force-dynamic" // force SSR
👉 In App Router:
Rendering is controlled at the data-fetching level, not just the page level.
Conclusion
There is no single “best” rendering method.
The best approach is to combine them based on your use case:
- Use SSG for performance and SEO
- Use SSR for real-time or user-specific data
- Use ISR for scalable freshness
- Use CSR for interactivity
- Use PPR for hybrid experiences
Choosing the right mix can significantly improve performance and scalability of your application.