Learn how caching and rendering strategies in Next.js 14 impact performance and scalability - with real examples and code for SSG, ISR, SSR, CSR & PPR.
Introduction
Building fast, scalable, and reliable web applications is no longer a luxury - itโs the baseline. Users expect instant load times, real-time content, and seamless interaction. As developers, weโre constantly balancing performance, freshness, and server cost.
This is where Next.js shines.
Next.js is not just a React framework - itโs an advanced platform that gives you multiple ways to control how data is fetched, rendered, and cached. With the introduction of the App Router, caching and rendering have evolved dramatically.
But hereโs the challenge:
โ Should I pre-render this page statically, or render it dynamically on every request?
โ How can I cache expensive fetches without making data stale?
โ What is ISR? Is it just a buzzword?
If these questions sound familiar, youโre in the right place.
What is Caching in Next.js?

Caching is temporarily storing data or content, so it can be reused instead of being recomputed or re-fetched every time. In web development, caching is key to speed, scalability, and resilience - and in Next.js, itโs deeply integrated into how your app is built and served.
Think of caching like a smart memory:
If youโve already done something once (like fetching data or rendering HTML), why do it again?
Instead of fetching fresh data on every request or rendering every page from scratch, you can store the result (HTML, JSON, API response, function output) and reuse it.
This avoids:
- Repeating expensive database/API calls
- Re-rendering the same output again and again
- Delaying the user experience
Why Do We Need Caching?
Caching ensures:
- โก๏ธ Faster responses (no unnecessary fetches/rendering)
- ๐ Lower server load (reuse responses across multiple requests)
- ๐ Efficient data freshness (controlled updates/revalidation)
- ๐ Enhanced scalability (cached data serves thousands)
Rendering Patterns Explained
Rendering is about one simple question:
โWhen and where should we generate the HTML for this page?โ
Next.js gives you full control over that answer - page by page, component by component. Each rendering pattern in Next.js has trade-offs in speed, freshness, and scalability, and knowing when to use which makes all the difference in building fast and maintainable applications.
Static Site Generation (SSG)
- HTML is generated once at build time.
- Served instantly from the file system or edge cache.
- Blazing fast and ideal for content that doesnโt change often.
SSG - How does it work?
When you run:
sh
Next.js:
- Calls your server functions (e.g. fetch users)
- Generates static HTML + JSON
- Saves it to disk or edge cache
SSG - Example
tsx
๐ก Important: If your getServerUserList() fetches from a live API, remember that the data will not update unless you re-build the app.
All users get the same version of the page, and it loads super fast.
SSG - Things to keep in mind
- If your data needs to change, SSG wonโt show updates unless you re-deploy.
- Good for read-only content.
- Use force-static to explicitly opt into SSG in the App Router.
Server-Side Rendering (SSR)
- HTML is generated on every request, on the server.
- Page data is always fresh - but slower and more expensive.
- Best for highly dynamic or user-specific content.
SSR - How does it work?
When a user visits your page:
- Next.js runs your server function (e.g. fetch users)
- Generates HTML on the fly
- Sends the response to the browser
This happens every single time the page is requested.
SSR - Example
tsx
Every time you refresh the page, it re-fetches the data and regenerates the HTML.
SSR - Things to keep in mind
- SSR can slow down under heavy traffic, since each request requires a full render.
- Great for dynamic content, but use it only when necessary.
- If the data can be cached or doesnโt change per user, consider ISR or SSG instead.
Incremental Static Regeneration (ISR)
- HTML is pre-rendered like SSG, but can be updated after deployment.
- Pages are cached and revalidated periodically or on demand.
- Perfect balance between speed and freshness.
ISR - How does ISR work?
- At build time, Next.js pre-renders selected pages into static HTML.
- These pages are served instantly from cache.
- After a time (e.g. 60 seconds), if a new request comes in:
- Next.js will re-generate the page in the background
- New version replaces the old cache
- Future users get the fresh version
So you get:
- Fast first loads โ
- Auto-updated content โ
- Low server load โ
ISR - Example without generateStaticParams
tsx
- First request: page is generated and cached.
- After 60s: the next request will re-generate the page.
ISR - Example with generateStaticParams
tsx
- First 5 pages are generated at build.
- Others are created on-demand when a user visits them.
- All pages are cached and auto-refreshed after 60s.
ISR - What does dynamicParams mean?
Setting | Behavior |
---|---|
dynamicParams: true | If the page was not pre-built, it will be rendered dynamically on request |
dynamicParams: false | If the page wasnโt generated during build, it will return 404 |
This gives you control over whatโs static and whatโs dynamic.
ISR - One Small revalidate Can Break Your Whole Page Caching โ ๏ธ
Letโs say youโre building a documentation site, and you want each page to revalidate only once a week:
tsx
Everything looks good. Pages are static, fast, and cost-effective.
Later, you add a system status bar to your layout that fetches API data every 60 seconds:
tsx
Now, every doc page revalidates every 1 minute - not 7 days.
๐ฅ Next.js uses the shortest revalidate value found anywhere in the render tree
That means layouts and deeply nested components can unintentionally lower the entire pageโs cache lifetime.
๐ก How to Avoid This
- Donโt put short-revalidate fetches in layouts or root components
- If you need live data like status, consider:
- Client-side rendering (CSR)
- Using Suspense + a dynamic boundary
- Moving the fetch to a useEffect if itโs purely visual
๐งช How to Detect It
Next.js wonโt warn you about this.
To find it:
- Open the browserโs Network tab
- Inspect page response headers:
- age โ how long the page has been cached
- cache-control โ how long itโs allowed to stay cached
- If the age is always low โ youโre revalidating more than you think

ISR - Things to keep in mind
- Avoid mixing components with very different revalidate times
- Donโt put live fetches into global layouts
- Use client-side rendering (useEffect) or suspense boundaries for small dynamic parts
- Audit your cache behavior regularly (e.g. in Vercelโs observability)
Partial Pre-Rendering (PPR)
- Part of the page is static, and part is dynamic.
- Uses React Server Components + Suspense.
- Great for layouts that should load instantly, while dynamic content streams in.
It allows you to pre-render only part of the page, while other parts are fetched and rendered dynamically on the server or client.
The goal?
Combine static speed with dynamic flexibility - on the same page.
PPR- How does it work?
PPR uses:
- React Server Components (RSC) โ for server-rendered static/dynamic content
- <Suspense> โ to separate parts of the page into dynamic units
- Optional client components โ for real-time or interactive data
Next.js streams the HTML:
- Static parts appear immediately
- Dynamic parts stream in as they load
- All this happens within a single page request
PPR - Example
Page layout (mixed rendering)
tsx
The dynamic client component
tsx

PPR - Things to keep in mind
- Use PPR when you want to keep layout and main content fast
- Wrap dynamic sections in <Suspense> to stream them separately
- Use "use client" components for data that changes per user or in real-time
- Keep dynamic parts small - donโt delay the entire page
Client-Side Rendering (CSR)
- HTML is mostly static; the data is fetched in the browser.
- Fully dynamic, great for interactivity.
- But slower initial load and SEO-unfriendly.
CSR - How does it work?
- Next.js sends a basic HTML shell to the browser.
- React loads and hydrates the components.
- Then your components fetch data on the client, using fetch() or useEffect().
This means:
- Nothing is fetched or rendered on the server
- You have maximum flexibility, but the page may load slower initially
CSR - Example
tsx
tsx
CSR - Things to keep in mind
- Use CSR for non-indexable, user-specific pages
- Show loading states clearly
- Prefetch critical data if possible
- Keep components lightweight to reduce hydration time
Function-Level Caching in Next.js โ๏ธ react.cache()
Next.js (thanks to React 18) allows you to cache the result of any async function, using react.cache().
This is called function-level caching - and itโs super useful for avoiding duplicated fetches or expensive calculations during server rendering.
โ When should you use it?
Use react.cache()
when:
- You want to deduplicate fetch calls within the same request
- Youโre calling the same function multiple times in one render tree
- You want fine-grained control without relying on full-page revalidation
It works best in:
- React Server Components
- Server-only functions
- Situations with internal caching needs
How does it work?
The first time you call the function with a given input, it runs and returns the result.
Subsequent calls (within the same request lifecycle) return the cached value, without re-running the logic.
This can save time, cost, and bandwidth - especially for expensive DB/API queries.
Example
typescript
tsx
โ ๏ธ Important Notes
- react.cache() only caches within the same request - itโs not global or cross-request.
- For persistent caching (e.g. across requests), use next: { revalidate } or edge/CDN caching.
- You must call it in a server-only context (not inside client components).
- This works well with React Server Components, especially when using the App Router.
Best Practices
- Wrap frequently used server functions with cache(...)
- Combine with revalidateTag() or revalidatePath() for smart invalidation
- Avoid overusing it for functions with high variability or user-specific data
Manual Revalidation in Next.js
While revalidate lets you define when a page or fetch should refresh automatically, manual revalidation gives you full control over when content gets updated - instantly, on demand.
This is perfect for:
- Admin panels (โRefresh posts nowโ)
- CMS integrations (โUpdate this pathโ)
- Invalidating cache after form submissions
- Saving costs by avoiding unnecessary revalidation
Two key APIs for on-demand revalidation
Function | Use case | Level |
---|---|---|
revalidateTag("...") | Invalidate all fetches using this tag | Fetch-level |
revalidatePath("/...") | Invalidate cached page for a specific path | Page-level |
revalidateTag() Example
You can tag your fetch requests like this:
typescript
Then, in your server action or route:
typescript
๐ก This will invalidate any cached data fetched with that tag - great for updating parts of the page without rebuilding.
revalidatePath() Example
typescript
This is useful when you want to regenerate the entire page instead of specific data.
Real-world example (admin-style refresh button)
tsx
API Route version
Sometimes, itโs better to revalidate from an external action (e.g. webhook, CMS, or button).
typescript
Then call it from the client or CMS webhook!
Best Practices
- Use revalidateTag() for data refresh, revalidatePath() for full-page updates
- Prefer server actions when possible - simple and fast
- Combine with cache() to control expensive fetches
- Monitor with DevTools or Vercel headers to verify cache invalidation
Rendering & Caching Strategy Decision Guide
Use case | Recommended Strategy |
---|---|
๐พ Static content, rarely changes | Static Site Generation (SSG) |
๐ฐ Content changes sometimes, but must be fast | Incremental Static Regeneration (ISR) |
๐ Live or per-user data (e.g. dashboard) | Server-Side Rendering (SSR) |
๐งฉ Part of the page is dynamic (e.g. sidebar) | Partial Pre-Rendering (PPR) |
๐โ๏ธ Logged-in user experience, no SEO needed | Client-Side Rendering (CSR) |
โก Prevent duplicate fetches within one request | Function-level caching with react.cache() |
๐ Trigger updates manually (e.g. via CMS) | revalidateTag() / revalidatePath() |
Full Comparison Table
Strategy | Data Freshness | Performance | SEO Friendly | Cache Control |
---|---|---|---|---|
SSG | โ Static only | โกโกโก Very fast | โ Yes | Built once at deploy |
ISR | ๐ก Periodic/on-demand | โกโก Fast | โ Yes | revalidate or tags |
SSR | โ Always fresh | โ ๏ธ Slower | โ Yes | No caching (per request) |
PPR | ๐ Mixed | โก Fast + Flex | โ Yes | Static + Suspense |
CSR | โ Always fresh | ๐ข Initial load | โ No | Browser-only |
react.cache() | โป๏ธ Per-request reuse | โก Faster renders | โ Yes | Code-level deduplication |
Quick Rules to Remember
- Always default to static (SSG or ISR) unless you need real-time content
- Use revalidateTag() to update selectively, without overfetching
- Donโt mix fast-revalidating fetches inside global layouts - it can silently break your caching
- Combine strategies: SSG + Suspense = PPR, SSR + react.cache() = Efficient SSR
- Watch response headers (age, x-vercel-cache) to debug caching in production
NEXT.js Rendering & Caching Decision Guide

Final Advice
Use caching not just to make things faster - use it to build smarter, cheaper, and more scalable apps. Next.js gives you the toolbox - you decide how to combine the pieces.