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.

Yash Patel4/7/20266 min read2 views
Next.js

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.tsx
2// This is SSG by default — no special config needed.
3
4export 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 });
3
4 const blogs = await payload.find({
5 collection: "blogs",
6 limit: 1000,
7 select: {
8 slug: true, // each will have a dynamic path
9 },
10 });
11
12 return blogs.docs
13 .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.tsx
2// Fresh data on every request — no caching.
3
4async function getContacts(session) {
5 const res = await fetch(`https://api.example.com/contacts`, {
6 cache: "no-store", // force fresh fetch every request
7 });
8 return res.json();
9}
10
11export default async function ContactsPage() {
12 const session = await getSession();
13 const data = await getContacts(session);
14
15 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.tsx
2// Static page that revalidates every 60 seconds.
3
4async function getProducts() {
5 const res = await fetch("<https://api.example.com/products>", {
6 next: { revalidate: 3600 }, // regenerate after 1 hour
7 });
8 return res.json();
9}
10
11export default async function ProductsPage() {
12 const products = await getProducts();
13
14 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'
3
4export function Page() {
5 const [data, setData] = useState(null)
6
7 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 }
16
17 fetchData().catch((e) => {
18 // handle the error as needed
19 console.error('An error occurred while fetching the data: ', e)
20 })
21 }, [])
22
23 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";
3
4export default async function ProductPage({
5 params,
6}: {
7 params: Promise<{ slug: string }>;
8}) {
9 const { slug } = await params;
10 const payload = await getPayload({ config });
11
12 const result = await payload.find({
13 collection: "products",
14 where: { slug: { equals: slug } },
15 limit: 1,
16 depth: 2,
17 });
18
19 const product = result.docs[0];
20 if (!product) notFound();
21
22 return (
23 <>
24 <div>
25 <p>{product.title}</p>
26 // other product details
27 </div>
28 <div>
29 <Suspense fallback={<p>Loading reviews...</p>}>
30 <Reviews productId={product.id} /> /* this will fetch product reviews using api or other
31 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 // ISR
4
5dynamic = "force-static" // force SSG
6dynamic = "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.