<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged cache-components</title>
<link>https://coderlegion.com/tag/cache-components</link>
<description>Powered by Question2Answer</description>
<item>
<title>Next.js Cache Components Migration Guide (2026)</title>
<link>https://coderlegion.com/26051/next-js-cache-components-migration-guide-2026</link>
<description>&lt;p&gt;You flip on &lt;code&gt;cacheComponents: true&lt;/code&gt; expecting a quick performance win, and instead your build explodes with red errors about &lt;code&gt;dynamic&lt;/code&gt;, &lt;code&gt;revalidate&lt;/code&gt;, and &lt;code&gt;fetchCache&lt;/code&gt; not being allowed anymore. Then, once those clear, the dev overlay starts flagging routes that &quot;won't render instantly&quot; — pointing at &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;searchParams&lt;/code&gt;, and database calls you've had in production for months.&lt;/p&gt;
&lt;p&gt;Nothing is actually broken. This is exactly how the migration is supposed to work — Next.js is walking you through moving from &lt;em&gt;implicit&lt;/em&gt; caching (where the framework guessed what should be static) to &lt;em&gt;explicit&lt;/em&gt; caching (where you say so directly). This guide walks through that migration end to end: every route segment config you'll need to replace, what to swap it for, and how to do it without rewriting your whole app in one sitting.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;New to Cache Components as a concept?&lt;/strong&gt; This guide is about &lt;em&gt;migrating&lt;/em&gt; an existing app. If you want the &quot;what and why&quot; first, read &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-rendering-strategies-explained&quot;&gt;Next.js Rendering Strategies Explained&lt;/a&gt;, which covers SSR, SSG, ISR, and Cache Components from scratch.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-migration-exists&quot;&gt;Why This Migration Exists&lt;/h3&gt;
&lt;p&gt;Before Cache Components, Next.js decided whether a route was static or dynamic &lt;em&gt;for you&lt;/em&gt;, by inspecting what your code touched — a &lt;code&gt;fetch&lt;/code&gt; call, a &lt;code&gt;cookies()&lt;/code&gt; read, a route segment config. That worked, but it was implicit: two developers could read the exact same route and disagree about whether it was static or dynamic, and a small code change could silently flip a page from instant to slow.&lt;/p&gt;
&lt;p&gt;Cache Components flips the default: &lt;strong&gt;everything is dynamic (rendered per-request) unless you explicitly mark it as cached.&lt;/strong&gt; You use &lt;code&gt;'use cache'&lt;/code&gt; to say &quot;cache this,&quot; &lt;code&gt;cacheLife()&lt;/code&gt; to say &quot;for how long,&quot; and &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; to say &quot;this part is allowed to be slow, stream it in separately.&quot; Nothing is guessed anymore.&lt;/p&gt;
&lt;p&gt;This is also what powers &lt;strong&gt;Partial Prerendering (PPR)&lt;/strong&gt; as the default behavior — a page can ship a static, instantly-served HTML shell while a personalized or slow-loading section streams in right behind it, all in one route, with no extra config.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;This guide assumes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt; — Cache Components requires it. If you're on Next.js 15 with &lt;code&gt;experimental.dynamicIO&lt;/code&gt; or &lt;code&gt;experimental.ppr&lt;/code&gt;, upgrade to 16 first using the &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/upgrading/version-16&quot;&gt;official upgrade guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App Router&lt;/strong&gt; — Cache Components is an App Router feature only. It does nothing for a &lt;code&gt;pages/&lt;/code&gt; directory. A hybrid app (both &lt;code&gt;pages/&lt;/code&gt; and &lt;code&gt;app/&lt;/code&gt;) is fine — the flag only affects &lt;code&gt;app/&lt;/code&gt; routes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode (the examples below use it, but everything works the same in plain JS).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js runtime&lt;/strong&gt; — Cache Components does not support &lt;code&gt;runtime = 'edge'&lt;/code&gt;. More on this later.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-enable-the-cachecomponents-flag&quot;&gt;Step 1: Enable the &lt;code&gt;cacheComponents&lt;/code&gt; Flag&lt;/h3&gt;
&lt;p&gt;Turn it on in your config file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// next.config.ts
import type { NextConfig } from &quot;next&quot;;

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; this single flag replaces three older, separate flags — &lt;code&gt;experimental.dynamicIO&lt;/code&gt;, &lt;code&gt;experimental.useCache&lt;/code&gt;, and &lt;code&gt;experimental.ppr&lt;/code&gt;. If your &lt;code&gt;next.config.ts&lt;/code&gt; still has any of those, delete them; &lt;code&gt;cacheComponents&lt;/code&gt; supersedes all three.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Leaving &lt;code&gt;experimental.ppr&lt;/code&gt; or &lt;code&gt;experimental_ppr&lt;/code&gt; route segment exports in place after adding &lt;code&gt;cacheComponents&lt;/code&gt;. Next.js 16 removed the &lt;code&gt;experimental.ppr&lt;/code&gt; flag entirely — PPR is just what &lt;code&gt;cacheComponents&lt;/code&gt; does now. Run &lt;code&gt;npx @next/codemod@canary&lt;/code&gt; to clean up leftover &lt;code&gt;experimental_ppr&lt;/code&gt; segment exports automatically (see the codemod list in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/upgrading/codemods&quot;&gt;official codemods reference&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The moment you save this file, any route that still exports &lt;code&gt;dynamic&lt;/code&gt;, &lt;code&gt;revalidate&lt;/code&gt;, or &lt;code&gt;fetchCache&lt;/code&gt; will fail to build. That's expected — those three route segment configs no longer exist under Cache Components, and the rest of this guide is about replacing each one.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-pick-your-migration-strategy&quot;&gt;Step 2: Pick Your Migration Strategy&lt;/h3&gt;
&lt;p&gt;You have two realistic paths, and the right one depends on how big your app is.&lt;/p&gt;
&lt;h4 id=&quot;option-a-migrate-everything-now&quot;&gt;Option A — Migrate everything now&lt;/h4&gt;
&lt;p&gt;Fine for small apps or a handful of routes. Go through each error the build throws, fix it, move to the next.&lt;/p&gt;
&lt;h4 id=&quot;option-b-migrate-incrementally-recommended-for-real-apps-&quot;&gt;Option B — Migrate incrementally (recommended for real apps)&lt;/h4&gt;
&lt;p&gt;Use the &lt;code&gt;instant&lt;/code&gt; route segment config to defer validation on routes you're not ready to touch yet, so the rest of your app keeps shipping while you convert routes one at a time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/layout.tsx
export const instant = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;instant = false&lt;/code&gt; tells Next.js &quot;this segment is allowed to block on request-time work — don't fail the build over it yet.&quot; It does &lt;strong&gt;not&lt;/strong&gt; force the route to become fully dynamic; a route that's actually prerenderable still ships a static shell even with this set. It just silences the validation error so you can come back to it later.&lt;/p&gt;
&lt;p&gt;Next.js ships a codemod that adds this opt-out to every &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;layout&lt;/code&gt;, and &lt;code&gt;default&lt;/code&gt; file in one pass:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @next/codemod@canary cache-components-instant-false ./app
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If your app lives under &lt;code&gt;src/app&lt;/code&gt;, pass that exact path instead — &lt;code&gt;npx @next/codemod@canary cache-components-instant-false ./src/app&lt;/code&gt;. A wrong path silently reports &lt;code&gt;0 ok&lt;/code&gt; instead of erroring, so check the file count in the output before assuming it worked.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With that run, your whole app builds and serves again. Then work through routes one at a time: remove &lt;code&gt;instant = false&lt;/code&gt; from a segment, fix whatever the dev overlay flags, repeat.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;&lt;code&gt;instant = false&lt;/code&gt; doesn't fix everything.&lt;/strong&gt; Synchronous, non-deterministic calls — &lt;code&gt;new Date()&lt;/code&gt;, &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, &lt;code&gt;crypto.randomUUID()&lt;/code&gt; — used during prerender still throw a hard build error that this opt-out does &lt;strong&gt;not&lt;/strong&gt; suppress. Move the call into a component wrapped in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; (calling &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/connection&quot;&gt;&lt;code&gt;connection()&lt;/code&gt;&lt;/a&gt; first) or into a Client Component instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-remove-dynamic-force-dynamic-&quot;&gt;Step 3: Remove &lt;code&gt;dynamic = 'force-dynamic'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This one's the easiest fix in the whole migration — you just delete it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export const dynamic = &quot;force-dynamic&quot;;

export default function Page() {
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — every route is already dynamic by default
export default function Page() {
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; under Cache Components, nothing is cached unless you say so with &lt;code&gt;'use cache'&lt;/code&gt;. &quot;Dynamic by default&quot; is now the baseline behavior, so &lt;code&gt;force-dynamic&lt;/code&gt; has nothing left to force.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-replace-dynamic-force-static-with-use-cache-&quot;&gt;Step 4: Replace &lt;code&gt;dynamic = 'force-static'&lt;/code&gt; with &lt;code&gt;'use cache'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Start by deleting the export, then let the build tell you what's actually uncached.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export const dynamic = &quot;force-static&quot;;

export default async function Page() {
  const data = await fetch(&quot;https://api.example.com/data&quot;);
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { cacheLife } from &quot;next/cache&quot;;

export default async function Page() {
  &quot;use cache&quot;;
  cacheLife(&quot;max&quot;);

  const data = await fetch(&quot;https://api.example.com/data&quot;);
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;use cache&lt;/code&gt; at the top of the function marks it as a cache boundary — everything inside, including the &lt;code&gt;fetch&lt;/code&gt; call, gets cached automatically. &lt;code&gt;cacheLife('max')&lt;/code&gt; is the closest built-in profile when you want long-lived caching, but it is not a literal equivalent of the old &lt;code&gt;force-static&lt;/code&gt; behavior.&lt;/p&gt;
&lt;p&gt;If your &lt;code&gt;force-static&lt;/code&gt; route reads runtime data (&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;), you can't keep it fully static anymore — that data genuinely only exists per-request. The fix there is wrapping just that part in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, covered in &lt;a rel=&quot;nofollow&quot; href=&quot;#update-your-invalidation-calls&quot;&gt;Step 9&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-5-replace-revalidate-with-cachelife&quot;&gt;Step 5: Replace &lt;code&gt;revalidate&lt;/code&gt; with &lt;code&gt;cacheLife&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;A numeric &lt;code&gt;revalidate&lt;/code&gt; export becomes a &lt;code&gt;cacheLife()&lt;/code&gt; call inside a &lt;code&gt;'use cache'&lt;/code&gt; function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export const revalidate = 3600; // 1 hour

export default async function Page() {
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { cacheLife } from &quot;next/cache&quot;;

export default async function Page() {
  &quot;use cache&quot;;
  cacheLife(&quot;hours&quot;);

  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;cacheLife&lt;/code&gt; accepts either a named profile string or a custom object. The built-in profiles — &lt;code&gt;seconds&lt;/code&gt;, &lt;code&gt;minutes&lt;/code&gt;, &lt;code&gt;hours&lt;/code&gt;, &lt;code&gt;days&lt;/code&gt;, &lt;code&gt;weeks&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt; — map to sensible defaults:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Profile &lt;/th&gt;&lt;th&gt;&lt;code&gt;stale&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;revalidate&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;expire&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;seconds&lt;/code&gt; &lt;/td&gt;&lt;td&gt;30s &lt;/td&gt;&lt;td&gt;1s &lt;/td&gt;&lt;td&gt;1 min&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;minutes&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;1 min &lt;/td&gt;&lt;td&gt;1 hour&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;hours&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;1 hour &lt;/td&gt;&lt;td&gt;1 day&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;days&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;1 day &lt;/td&gt;&lt;td&gt;1 week&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;weeks&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;1 week &lt;/td&gt;&lt;td&gt;30 days&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;max&lt;/code&gt; &lt;/td&gt;&lt;td&gt;5 min &lt;/td&gt;&lt;td&gt;30 days &lt;/td&gt;&lt;td&gt;1 year&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If none of the presets match your old &lt;code&gt;revalidate&lt;/code&gt; value exactly, pick the closest one, or define a custom profile in &lt;code&gt;next.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// next.config.ts
import type { NextConfig } from &quot;next&quot;;

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    blog: {
      stale: 3600, // 1 hour — how long the client can use a cached value without checking
      revalidate: 900, // 15 minutes — how often the server refreshes in the background
      expire: 86400, // 1 day — max age before the entry is dropped entirely
    },
  },
};

export default nextConfig;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;stale&lt;/code&gt; controls how long the client Router Cache can reuse the result; it does not set the HTTP &lt;code&gt;Cache-Control: max-age&lt;/code&gt; header.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/page.tsx
import { cacheLife } from &quot;next/cache&quot;;

export default async function BlogPage() {
  &quot;use cache&quot;;
  cacheLife(&quot;blog&quot;); // references the custom profile above

  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; You can also redefine a built-in profile (including &lt;code&gt;default&lt;/code&gt;) with the same name, if you want &lt;code&gt;cacheLife('hours')&lt;/code&gt; to mean something different across your whole app. Document it somewhere obvious if you do — a teammate reading &lt;code&gt;cacheLife('hours')&lt;/code&gt; will otherwise assume the stock 1-hour behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-6-delete-fetchcache&quot;&gt;Step 6: Delete &lt;code&gt;fetchCache&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This route segment config has no replacement because it has no purpose anymore.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export const fetchCache = &quot;force-cache&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — just delete it
export default async function Page() {
  &quot;use cache&quot;;
  // every fetch() inside this scope is cached automatically
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; &lt;code&gt;fetchCache&lt;/code&gt; used to control whether &lt;code&gt;fetch()&lt;/code&gt; calls were cached at the route level. Under Cache Components, caching is decided per-function by &lt;code&gt;'use cache'&lt;/code&gt;, not globally per route — so there's nothing left for &lt;code&gt;fetchCache&lt;/code&gt; to configure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-7-move-fetch-cache-options-into-use-cache-&quot;&gt;Step 7: Move &lt;code&gt;fetch()&lt;/code&gt; Cache Options into &lt;code&gt;'use cache'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Move the caching policy into the &lt;code&gt;use cache&lt;/code&gt; scope and replace fetch-level tags with &lt;code&gt;cacheTag()&lt;/code&gt; where appropriate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export default async function Page() {
  const res = await fetch(&quot;https://api.example.com/data&quot;, {
    cache: &quot;force-cache&quot;,
    next: { revalidate: 3600, tags: [&quot;data&quot;] },
  });
  const data = await res.json();
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { cacheLife, cacheTag } from &quot;next/cache&quot;;

async function getData() {
  &quot;use cache&quot;;
  cacheLife(&quot;hours&quot;);
  cacheTag(&quot;data&quot;);

  const res = await fetch(&quot;https://api.example.com/data&quot;);
  return res.json();
}

export default async function Page() {
  const data = await getData();
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; pulling the fetch into its own &lt;code&gt;getData()&lt;/code&gt; function marked &lt;code&gt;'use cache'&lt;/code&gt; is the standard pattern — it keeps the cache boundary small and lets the page component stay a plain async function. &lt;code&gt;cacheTag('data')&lt;/code&gt; is what lets you invalidate this specific cached result later (&lt;a rel=&quot;nofollow&quot; href=&quot;#update-your-invalidation-calls&quot;&gt;Step 9&lt;/a&gt;).&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Persistence difference worth knowing:&lt;/strong&gt; the old &lt;code&gt;fetch&lt;/code&gt; Data Cache persisted across deployments and serverless instances. By default, 'use cache' uses an in-memory cache isolated to the Next.js process. Multiple instances do not share that cache, and entries are lost when the process restarts. If you need caching that survives instance teardown, use &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/directives/use-cache-remote&quot;&gt;&lt;code&gt;'use cache: remote'&lt;/code&gt;&lt;/a&gt; or configure a &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheHandlers&quot;&gt;cache handler&lt;/a&gt;. Either way, expect values to recompute after a fresh deployment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-8-replace-unstable-cache-with-use-cache-&quot;&gt;Step 8: Replace &lt;code&gt;unstable_cache&lt;/code&gt; with &lt;code&gt;'use cache'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;If you were wrapping database queries in &lt;code&gt;unstable_cache&lt;/code&gt;, the migration is mechanical.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
import { unstable_cache } from &quot;next/cache&quot;;
import { db } from &quot;@/lib/db&quot;;

export const getUser = unstable_cache(
  async (id: string) =&amp;gt; {
    return db.query.users.findFirst({ where: eq(users.id, id) });
  },
  [&quot;user&quot;], // cache key prefix
  { tags: [&quot;users&quot;], revalidate: 3600 }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { cacheLife, cacheTag } from &quot;next/cache&quot;;
import { db } from &quot;@/lib/db&quot;;

export async function getUser(id: string) {
  &quot;use cache&quot;;
  cacheLife(&quot;hours&quot;);
  cacheTag(&quot;users&quot;);

  return db.query.users.findFirst({ where: eq(users.id, id) });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the wrapper function becomes a plain function with a directive. Notice the manual &lt;code&gt;[&quot;user&quot;]&lt;/code&gt; cache-key-prefix array is gone entirely — Next.js derives the cache key automatically from the function's arguments (and its closure), so you never write a cache key by hand.&lt;/p&gt;
&lt;p&gt;Same persistence caveat as Step 7 applies: &lt;code&gt;unstable_cache&lt;/code&gt; persisted across deployments; &lt;code&gt;'use cache'&lt;/code&gt; doesn't by default.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;update-your-invalidation-calls&quot;&gt;Step 9: Update Your Invalidation Calls&lt;/h2&gt;
&lt;p&gt;On-demand invalidation still works — you just have three tools now instead of one, and picking the right one matters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/updateTag&quot;&gt;&lt;code&gt;updateTag&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — for a mutation where the user needs to see their own change &lt;em&gt;immediately&lt;/em&gt; (read-your-own-writes). Only callable from a Server Action.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/revalidateTag&quot;&gt;&lt;code&gt;revalidateTag&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — for stale-while-revalidate behavior. Works in both Server Actions and Route Handlers, but now &lt;strong&gt;requires a cache profile as a second argument&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/revalidatePath&quot;&gt;&lt;code&gt;revalidatePath&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — unchanged from before.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/actions.ts
&quot;use server&quot;;

import { updateTag } from &quot;next/cache&quot;;

export async function createPost(formData: FormData) {
  // create the post in your database, then:
  updateTag(&quot;posts&quot;); // the next render sees the new post immediately
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/webhook/route.ts

// Before
import { revalidateTag } from &quot;next/cache&quot;;

export async function POST() {
  revalidateTag(&quot;posts&quot;);
  return Response.json({ ok: true });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/webhook/route.ts

// After — a profile is now required
import { revalidateTag } from &quot;next/cache&quot;;

export async function POST() {
  revalidateTag(&quot;posts&quot;, &quot;max&quot;);
  return Response.json({ ok: true });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the bare, single-argument &lt;code&gt;revalidateTag(tag)&lt;/code&gt; call is deprecated — it still technically compiles if you suppress the TypeScript error, but it may stop working in a future release. Pass &lt;code&gt;max&lt;/code&gt; as the recommended default — it uses the profile's one-year &lt;code&gt;expire&lt;/code&gt; value as the stale-serving window, so requests almost never block waiting on a fresh fetch after invalidation.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Reaching for &lt;code&gt;revalidateTag&lt;/code&gt; after a mutation when you actually want &lt;code&gt;updateTag&lt;/code&gt;. &lt;code&gt;revalidateTag&lt;/code&gt; marks data stale and serves the &lt;em&gt;old&lt;/em&gt; value while it refreshes in the background — a user who just submitted a form could still see their old data on the very next page load. If the user needs to see their own change right away, use &lt;code&gt;updateTag&lt;/code&gt; inside the Server Action instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-10-delete-unstable-nostore&quot;&gt;Step 10: Delete &lt;code&gt;unstable_noStore&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Remove &lt;code&gt;unstable_noStore()&lt;/code&gt; in most migrations. If the operation must intentionally happen at request time and doesn't naturally depend on &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or another request API, use &lt;code&gt;connection()&lt;/code&gt; to opt that work out of prerendering.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
import { unstable_noStore as noStore } from &quot;next/cache&quot;;

export default async function Page() {
  noStore();
  const data = await db.query(&quot;...&quot;);
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
export default async function Page() {
  const data = await db.query(&quot;...&quot;);
  return &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a component genuinely needs to run at request time (not just &quot;not be cached&quot;), call &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/connection&quot;&gt;&lt;code&gt;connection()&lt;/code&gt;&lt;/a&gt; before the work and wrap it in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; — that's the real replacement when you need to force a dynamic boundary rather than just skip caching.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-11-fix-generatestaticparams&quot;&gt;Step 11: Fix &lt;code&gt;generateStaticParams&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Two real changes here for dynamic routes.&lt;/p&gt;
&lt;h4 id=&quot;an-empty-array-is-now-an-error&quot;&gt;An empty array is now an error&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — used to defer every path to runtime, now throws a build error
export async function generateStaticParams() {
  return [];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — return at least one param so Next.js can prerender a shell
export async function generateStaticParams() {
  const posts = await fetch(&quot;https://.../posts&quot;).then((res) =&amp;gt; res.json());
  return posts.slice(0, 1).map((post: { slug: string }) =&amp;gt; ({ slug: post.slug }));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Cache Components needs &lt;code&gt;generateStaticParams&lt;/code&gt; to produce at least one real param so it can prerender a static shell and confirm it's non-empty. Paths you don't return still work fine — Next.js prerenders a generic shell for unknown params and streams the real content at request time.&lt;/p&gt;
&lt;h4 id=&quot;dynamicparams-is-no-longer-supported&quot;&gt;&lt;code&gt;dynamicParams&lt;/code&gt; is no longer supported&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — this now fails the build entirely
export const dynamicParams = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Delete it. If you were using &lt;code&gt;dynamicParams: false&lt;/code&gt; to reject unknown params, call &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/not-found&quot;&gt;&lt;code&gt;notFound()&lt;/code&gt;&lt;/a&gt; inside the page instead, once you know the param doesn't resolve to real data.&lt;/p&gt;
&lt;h4 id=&quot;await-params-inside-lt-suspense-gt-not-at-the-top&quot;&gt;Await &lt;code&gt;params&lt;/code&gt; inside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, not at the top&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — awaiting at the top blocks the whole static shell
export default async function Page({
  params,
}: {
  params: Promise&amp;lt;{ slug: string }&amp;gt;;
}) {
  const { slug } = await params;
  return &amp;lt;Post slug={slug} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After
import { Suspense } from &quot;react&quot;;

export default function Page({ params }: PageProps&amp;lt;&quot;/blog/[slug]&quot;&amp;gt;) {
  return (
    &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;Post params={params} /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}

async function Post({ params }: Pick&amp;lt;PageProps&amp;lt;&quot;/blog/[slug]&quot;&amp;gt;, &quot;params&quot;&amp;gt;) {
  const { slug } = await params;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; passing the &lt;code&gt;params&lt;/code&gt; promise straight through as a prop, then awaiting it inside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;-wrapped child, lets Next.js prerender the shell for unknown/unlisted params instead of blocking the entire page on resolving them first.&lt;/p&gt;
&lt;p&gt;The same pattern applies to &lt;code&gt;usePathname&lt;/code&gt;, &lt;code&gt;useParams&lt;/code&gt;, &lt;code&gt;useSelectedLayoutSegment&lt;/code&gt;, &lt;code&gt;useSelectedLayoutSegments&lt;/code&gt;, and &lt;code&gt;useSearchParams&lt;/code&gt; in Client Components — if the value depends on a dynamic param, wrap the smallest component that reads it in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; rather than the whole tree.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-12-wrap-cookies-headers-and-searchparams-in-lt-suspense-gt-&quot;&gt;Step 12: Wrap &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, and &lt;code&gt;searchParams&lt;/code&gt; in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This is the change you'll hit most often, because it's the most common source of &quot;instant navigation&quot; validation errors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — reading cookies at the top makes the whole route dynamic
import { cookies } from &quot;next/headers&quot;;

export default async function Page() {
  const theme = (await cookies()).get(&quot;theme&quot;)?.value;
  return &amp;lt;Dashboard theme={theme} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — the page shell prerenders; only Dashboard streams at request time
import { cookies } from &quot;next/headers&quot;;
import { Suspense } from &quot;react&quot;;

export default function Page() {
  return (
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;}&amp;gt;
      &amp;lt;Dashboard /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}

async function Dashboard() {
  const theme = (await cookies()).get(&quot;theme&quot;)?.value;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; moving the &lt;code&gt;cookies()&lt;/code&gt; read into a separate component, and wrapping just that component in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, lets everything &lt;em&gt;outside&lt;/em&gt; it (the page's static shell) prerender normally. Only the part that genuinely needs per-request data waits until the actual request.&lt;/p&gt;
&lt;p&gt;The same move applies to &lt;code&gt;searchParams&lt;/code&gt;, which your page receives as a promise:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { Suspense } from &quot;react&quot;;

export default function Page({ searchParams }: PageProps&amp;lt;&quot;/&quot;&amp;gt;) {
  return (
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;}&amp;gt;
      &amp;lt;Results searchParams={searchParams} /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}

async function Results({ searchParams }: Pick&amp;lt;PageProps&amp;lt;&quot;/&quot;&amp;gt;, &quot;searchParams&quot;&amp;gt;) {
  const { query } = await searchParams;
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If a cookie or header controls an attribute on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; in the root layout (&lt;code&gt;lang&lt;/code&gt;, &lt;code&gt;dir&lt;/code&gt;, &lt;code&gt;data-theme&lt;/code&gt;), there's no child element to wrap in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; — reading it there makes the whole subtree request-bound by nature. Use an inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; that sets the attribute client-side before paint instead; see &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/preventing-flash-before-hydration&quot;&gt;Preventing flash before hydration&lt;/a&gt; for the pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-13-update-get-route-handlers&quot;&gt;Step 13: Update &lt;code&gt;GET&lt;/code&gt; Route Handlers&lt;/h3&gt;
&lt;p&gt;Route Handlers follow the same rules as pages now.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/products/route.ts

// Before
export const dynamic = &quot;force-static&quot;;

export async function GET() {
  const products = await db.query(&quot;SELECT * FROM products&quot;);
  return Response.json(products);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/products/route.ts

// After
import { cacheLife } from &quot;next/cache&quot;;

export async function GET() {
  const products = await getProducts();
  return Response.json(products);
}

async function getProducts() {
  &quot;use cache&quot;;
  cacheLife(&quot;hours&quot;);

  return db.query(&quot;SELECT * FROM products&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the &lt;code&gt;'use cache'&lt;/code&gt; directive can't be applied to the exported &lt;code&gt;GET&lt;/code&gt; function itself — it goes on a helper function the handler calls instead.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Reading uncached or runtime data in a &lt;code&gt;GET&lt;/code&gt; handler bails out of prerendering by &lt;em&gt;throwing&lt;/em&gt;. If you already wrap your handler body in &lt;code&gt;try/catch&lt;/code&gt;, that block will catch this bail-out too — which can add noisy logs during the build. Set &lt;code&gt;experimental.hideLogsAfterAbort: true&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt; if that gets in your way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-14-handle-generatemetadata-and-generateviewport&quot;&gt;Step 14: Handle &lt;code&gt;generateMetadata&lt;/code&gt; and &lt;code&gt;generateViewport&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;These follow the same component rules — cache external data, or explicitly mark the page as needing runtime data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
export async function generateMetadata() {
  const { title, description } = await db.query(&quot;site-metadata&quot;);
  return { title, description };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// After — cache it, same as any other external data fetch
export async function generateMetadata() {
  &quot;use cache&quot;;
  const { title, description } = await db.query(&quot;site-metadata&quot;);
  return { title, description };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your metadata genuinely depends on runtime data (a signed-in user's name, a cookie), you can't wrap &lt;code&gt;generateMetadata&lt;/code&gt; itself in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; — instead, add a small dynamic marker component to the page so the static content still prerenders while the metadata resolves separately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { Suspense } from &quot;react&quot;;
import { connection } from &quot;next/server&quot;;

export async function generateMetadata() {
  // reads runtime data — intentional
  return { title: &quot;Personalized Title&quot; };
}

async function Connection() {
  await connection();
  return null;
}

export default function Page() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;article&amp;gt;Static content&amp;lt;/article&amp;gt;
      &amp;lt;Suspense&amp;gt;
        &amp;lt;Connection /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the full breakdown of trade-offs here, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/generate-metadata#with-cache-components&quot;&gt;&lt;code&gt;generateMetadata&lt;/code&gt; with Cache Components&lt;/a&gt; in the official reference — this is one of the trickier corners of the migration and worth reading directly if metadata personalization matters for your app. If you haven't set up metadata at all yet, &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-metadata&quot;&gt;How to Add Metadata in Next.js 16&lt;/a&gt; covers the basics first.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-15-remove-runtime-edge-&quot;&gt;Step 15: Remove &lt;code&gt;runtime = 'edge'&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Cache Components requires the Node.js runtime — Edge isn't supported.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before — no longer supported under Cache Components
export const runtime = &quot;edge&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Delete the export (Node.js is the default anyway). If you specifically need Edge behavior for a route — geolocation checks, ultra-low-latency redirects — reach for &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/file-conventions/proxy&quot;&gt;Proxy&lt;/a&gt; (the renamed &lt;code&gt;middleware.ts&lt;/code&gt;) instead of the Edge runtime on the route itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-gotcha-after-the-build-passes-ui-state-no-longer-resets&quot;&gt;A Gotcha After the Build Passes: UI State No Longer Resets&lt;/h3&gt;
&lt;p&gt;Once your routes build cleanly, there's one behavior change worth testing manually, because it won't show up as a build error: Cache Components preserves component state across client-side navigation using React's &lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/reference/react/Activity&quot;&gt;&lt;code&gt;&amp;lt;Activity&amp;gt;&lt;/code&gt;&lt;/a&gt; component, instead of unmounting the page you navigated away from.&lt;/p&gt;
&lt;p&gt;In practice: a &lt;code&gt;useState&lt;/code&gt; value, an open dropdown, or scroll position no longer resets just because the user clicked to a different page and back. This is usually a win, but it can surface bugs in code that assumed navigating away meant a clean remount — a dropdown that used to close automatically, a dialog whose focus-on-open effect no longer refires because its state was preserved rather than reset.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Assuming a modal or dropdown will close itself on navigation the way it always used to. If a component should reset when its route or user context changes, reset that state explicitly based on the relevant prop/route change instead of relying on unmount behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;migration-checklist&quot;&gt;Migration Checklist&lt;/h3&gt;
&lt;p&gt;Work through this in order for each route:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;cacheComponents: true&lt;/code&gt; set in &lt;code&gt;next.config.ts&lt;/code&gt;; old &lt;code&gt;experimental.dynamicIO&lt;/code&gt; / &lt;code&gt;experimental.useCache&lt;/code&gt; / &lt;code&gt;experimental.ppr&lt;/code&gt; removed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;dynamic = 'force-dynamic'&lt;/code&gt; deleted everywhere&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;dynamic = 'force-static'&lt;/code&gt; replaced with &lt;code&gt;'use cache'&lt;/code&gt; + &lt;code&gt;cacheLife('max')&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;revalidate&lt;/code&gt; exports replaced with &lt;code&gt;cacheLife()&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;fetchCache&lt;/code&gt; exports deleted&lt;/li&gt;
&lt;li&gt;[ ] Individual &lt;code&gt;fetch()&lt;/code&gt; cache options (&lt;code&gt;cache&lt;/code&gt;, &lt;code&gt;next.revalidate&lt;/code&gt;, &lt;code&gt;next.tags&lt;/code&gt;) moved into &lt;code&gt;'use cache'&lt;/code&gt; functions with &lt;code&gt;cacheLife&lt;/code&gt; / &lt;code&gt;cacheTag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;unstable_cache&lt;/code&gt; calls converted to &lt;code&gt;'use cache'&lt;/code&gt; functions&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;revalidateTag&lt;/code&gt; calls updated to pass a cache profile (&lt;code&gt;revalidateTag(tag, 'max')&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Mutations that need read-your-own-writes use &lt;code&gt;updateTag&lt;/code&gt; instead of &lt;code&gt;revalidateTag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;unstable_noStore()&lt;/code&gt; calls removed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;generateStaticParams&lt;/code&gt; returns at least one param; &lt;code&gt;dynamicParams&lt;/code&gt; export removed&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;searchParams&lt;/code&gt; awaited inside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;-wrapped children, not at the top of the page&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;cookies()&lt;/code&gt; / &lt;code&gt;headers()&lt;/code&gt; reads pushed into &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;-wrapped components&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;GET&lt;/code&gt; Route Handlers use a &lt;code&gt;'use cache'&lt;/code&gt; helper instead of &lt;code&gt;dynamic = 'force-static'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;generateMetadata&lt;/code&gt; / &lt;code&gt;generateViewport&lt;/code&gt; cache external data or use the dynamic-marker pattern for runtime data&lt;/li&gt;
&lt;li&gt;[ ] No routes still export &lt;code&gt;runtime = 'edge'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] UI that relied on unmount-to-reset behavior (dropdowns, dialogs, forms) tested manually after migration&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;do-i-have-to-migrate-my-whole-app-at-once-&quot;&gt;Do I have to migrate my whole app at once?&lt;/h4&gt;
&lt;p&gt;No. Use &lt;code&gt;export const instant = false&lt;/code&gt; on the layouts or pages that aren't ready, run the &lt;code&gt;cache-components-instant-false&lt;/code&gt; codemod to apply it everywhere in one pass, and convert routes one at a time from there. Your app keeps building and serving the whole time.&lt;/p&gt;
&lt;h4 id=&quot;what-happens-to-my-existing-fetch-cache-and-unstable-cache-data-during-migration-&quot;&gt;What happens to my existing &lt;code&gt;fetch&lt;/code&gt; cache and &lt;code&gt;unstable_cache&lt;/code&gt; data during migration?&lt;/h4&gt;
&lt;p&gt;Existing &lt;code&gt;unstable_cache&lt;/code&gt; usage is still supported, but Next.js 16 recommends migrating it to &lt;code&gt;use cache&lt;/code&gt;. The old fetch/Data Cache model and Cache Components can coexist during migration, but route segment configs such as &lt;code&gt;dynamic&lt;/code&gt;, &lt;code&gt;revalidate&lt;/code&gt;, and &lt;code&gt;fetchCache&lt;/code&gt; must be migrated.&lt;/p&gt;
&lt;h4 id=&quot;why-does-my-build-fail-on-new-date-even-with-instant-false-set-&quot;&gt;Why does my build fail on &lt;code&gt;new Date()&lt;/code&gt; even with &lt;code&gt;instant = false&lt;/code&gt; set?&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;instant = false&lt;/code&gt; only defers the &lt;em&gt;validation&lt;/em&gt; check for whether a route renders instantly — it doesn't clear synchronous, non-deterministic calls like &lt;code&gt;new Date()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, or &lt;code&gt;crypto.randomUUID()&lt;/code&gt; used during prerendering. Those still throw a hard build error. Move the call into a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;-wrapped component (calling &lt;code&gt;connection()&lt;/code&gt; first) or into a Client Component.&lt;/p&gt;
&lt;h4 id=&quot;is-use-cache-the-same-as-the-old-fetch-data-cache-&quot;&gt;Is &lt;code&gt;'use cache'&lt;/code&gt; the same as the old &lt;code&gt;fetch&lt;/code&gt; Data Cache?&lt;/h4&gt;
&lt;p&gt;No, and this trips people up. The old &lt;code&gt;fetch&lt;/code&gt; Data Cache persisted across deployments and serverless instances. The default &lt;code&gt;use cache&lt;/code&gt; handler uses an in-memory LRU cache isolated to each Next.js process. It isn't shared across multiple instances and is lost when the process restarts. Use &lt;code&gt;use cache: remote&lt;/code&gt; or a cache handler if you need durability across deployments.&lt;/p&gt;
&lt;h4 id=&quot;my-revalidatetag-tag-call-now-shows-a-typescript-error-what-changed-&quot;&gt;My &lt;code&gt;revalidateTag(tag)&lt;/code&gt; call now shows a TypeScript error — what changed?&lt;/h4&gt;
&lt;p&gt;The single-argument form is deprecated. &lt;code&gt;revalidateTag&lt;/code&gt; now requires a cache profile as a second argument — &lt;code&gt;revalidateTag('posts', 'max')&lt;/code&gt; uses the recommended stale-while-revalidate profile. The &lt;code&gt;max&lt;/code&gt; cache profile has a &lt;code&gt;30-day&lt;/code&gt; server revalidation interval and a &lt;code&gt;1-year&lt;/code&gt; expiration window.&lt;/p&gt;
&lt;h4 id=&quot;does-cache-components-work-with-the-pages-router-&quot;&gt;Does Cache Components work with the Pages Router?&lt;/h4&gt;
&lt;p&gt;No — it's an App Router-only feature. If your project has both &lt;code&gt;pages/&lt;/code&gt; and &lt;code&gt;app/&lt;/code&gt; directories, &lt;code&gt;cacheComponents: true&lt;/code&gt; only affects the &lt;code&gt;app/&lt;/code&gt; routes; &lt;code&gt;pages/&lt;/code&gt; routes are completely unaffected and need no changes.&lt;/p&gt;
&lt;h4 id=&quot;can-i-use-cache-components-on-vercel-s-edge-runtime-&quot;&gt;Can I use Cache Components on Vercel's Edge Runtime?&lt;/h4&gt;
&lt;p&gt;No. Cache Components requires the Node.js runtime. Remove any &lt;code&gt;runtime = 'edge'&lt;/code&gt; export from routes you're migrating — if you specifically need Edge-level behavior, use Proxy (&lt;code&gt;proxy.ts&lt;/code&gt;) instead of the route-level Edge runtime.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;The Cache Components migration looks scary because it surfaces every implicit assumption your app was making about what's static versus dynamic, all at once, as build errors. But each fix is genuinely mechanical: &lt;code&gt;revalidate&lt;/code&gt; becomes &lt;code&gt;cacheLife&lt;/code&gt;, &lt;code&gt;unstable_cache&lt;/code&gt; becomes a &lt;code&gt;'use cache'&lt;/code&gt; function, and any runtime data read (&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;searchParams&lt;/code&gt;, a live database query) gets pushed into a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary so it doesn't block the rest of the page.&lt;/p&gt;
&lt;p&gt;Start with &lt;code&gt;instant = false&lt;/code&gt; on everything you're not ready to touch, get the app building again, then convert routes one at a time using the checklist above. You'll come out the other side with a codebase where every cache decision is explicit instead of guessed — and a meaningfully faster app for it.&lt;/p&gt;
&lt;h3 id=&quot;continue-learning&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-rendering-strategies-explained&quot;&gt;Next.js Rendering Strategies Explained: SSR, SSG, CSR, ISR &amp;amp; Cache Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-route-handlers&quot;&gt;Route Handlers Explained in Next.js 16 (Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-metadata&quot;&gt;How to Add Metadata in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/error-handling-in-nextjs-16&quot;&gt;Error Handling in Next.js 16: The Complete 2026 Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/server-vs-client-components-nextjs-16&quot;&gt;Server vs Client Components in Next.js 16 (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;useful-resources&quot;&gt;Useful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/migrating-to-cache-components&quot;&gt;Official Guide: Migrating to Cache Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents&quot;&gt;cacheComponents Config Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/cacheLife&quot;&gt;cacheLife Function Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/revalidateTag&quot;&gt;revalidateTag Function Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/instant-navigation&quot;&gt;Instant Navigation Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26051/next-js-cache-components-migration-guide-2026</guid>
<pubDate>Wed, 02 Sep 2026 04:01:53 +0000</pubDate>
</item>
<item>
<title>Next.js Rendering Strategies Explained: SSR, SSG, CSR, ISR &amp; Cache Components (2026)</title>
<link>https://coderlegion.com/23657/next-js-rendering-strategies-explained-ssr-ssg-csr-isr-cache-components-2026</link>
<description>&lt;p&gt;Ever opened the Next.js docs and seen acronyms like SSR, SSG, CSR, and ISR thrown around like everyone's supposed to just &lt;em&gt;know&lt;/em&gt; what they mean? You're not alone. Half the confusion around Next.js performance comes down to one simple question nobody explains clearly enough: &lt;strong&gt;when does your page actually get built — and where?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Once you understand that one question, every rendering strategy in Next.js suddenly makes sense. By the end of this guide, you'll know exactly what SSR, SSG, CSR, and ISR mean, how the newer &lt;strong&gt;Cache Components&lt;/strong&gt; model (powered by &lt;strong&gt;Partial Prerendering&lt;/strong&gt;, or PPR) ties it all together, and which one to reach for in your own projects.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;Every page you build has to answer one question: &lt;strong&gt;&quot;When should the HTML for this page get generated?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are really only three possible answers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ahead of time&lt;/strong&gt;, during &lt;code&gt;next build&lt;/code&gt; — before any user even visits the site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On request&lt;/strong&gt;, every time a user visits — generated fresh on the server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In the browser&lt;/strong&gt;, after the page loads — generated by JavaScript running on the user's device.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Next.js gives you a strategy for each answer, plus a smart hybrid (Cache Components) that mixes them on the &lt;em&gt;same page&lt;/em&gt;. Here's the whole picture at a glance before we go through each one:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devstacked.tech/nextjs-rendering-strategies-explained/image1.svg&quot; alt=&quot;Diagram comparing when SSR, SSG, CSR, ISR, and Cache Components render — across build time, request time, and the browser&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-ssr-server-side-rendering-&quot;&gt;What Is SSR (Server-Side Rendering)?&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; ordering food at a restaurant — you ask, the kitchen cooks it fresh, then it's served to you. Every single time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SSR means the HTML for a page is generated &lt;strong&gt;on the server, for every single request&lt;/strong&gt;. The user always gets fresh data, but the server has to do work on every visit.&lt;/p&gt;
&lt;p&gt;In the Next.js App Router, you get SSR automatically whenever a page reads something that can only be known at request time — like cookies, headers, or search params — or when you explicitly opt a page into dynamic rendering.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/page.tsx
import { cookies } from &quot;next/headers&quot;;

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const userId = cookieStore.get(&quot;userId&quot;)?.value;

  const res = await fetch(`https://api.example.com/users/${userId}`, {
    cache: &quot;no-store&quot;, // tells Next.js: never cache this, always fetch fresh
  });
  const user = await res.json();

  return &amp;lt;h1&amp;gt;Welcome back, {user.name}&amp;lt;/h1&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; because we read &lt;code&gt;cookies()&lt;/code&gt; and pass &lt;code&gt;cache: &quot;no-store&quot;&lt;/code&gt; to &lt;code&gt;fetch&lt;/code&gt;, Next.js knows this page depends on request-specific data. It renders the page on the server every time someone visits, instead of reusing a cached copy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use SSR when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The content is personalized per user (dashboards, account pages).&lt;/li&gt;
&lt;li&gt;Data changes frequently and must always be fresh (stock prices, live order status).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Using SSR for content that rarely changes (like a blog post) wastes server resources. If the data isn't user-specific or constantly changing, you probably want SSG or ISR instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-ssg-static-site-generation-&quot;&gt;What Is SSG (Static Site Generation)?&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; a printed menu — it's made once, in advance, and handed to every customer exactly the same.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SSG means the HTML is generated &lt;strong&gt;once, at build time&lt;/strong&gt;, and then reused for every visitor. No server work happens per request — Next.js just serves the same pre-built HTML file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPosts } from &quot;@/lib/posts&quot;;

// This tells Next.js which pages to pre-build at build time
export async function generateStaticParams() {
  const posts = getAllPosts();
  return posts.map((post) =&amp;gt; ({ slug: post.slug }));
}

export default async function BlogPostPage({
  params,
}: {
  params: Promise&amp;lt;{ slug: string }&amp;gt;;
}) {
  const { slug } = await params;
  const post = getPostBySlug(slug);

  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{post.description}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;generateStaticParams()&lt;/code&gt; tells Next.js exactly which blog post pages exist. At build time, Next.js renders each one into static HTML. When a real visitor requests &lt;code&gt;/blog/my-post&lt;/code&gt;, the server just hands over the already-built file — no rendering work, no database hit, nothing. It's instant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use SSG when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content doesn't change often (blog posts, marketing pages, documentation).&lt;/li&gt;
&lt;li&gt;You want the fastest possible load times and lowest server cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; If your content updates frequently but you're using pure SSG, visitors will keep seeing stale data until you rebuild and redeploy the entire site. That's exactly the problem ISR solves — keep reading.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-csr-client-side-rendering-&quot;&gt;What Is CSR (Client-Side Rendering)?&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; ordering a build-it-yourself meal kit — the server hands you the box and instructions (a minimal HTML shell), but your browser does the actual cooking (fetching data and rendering the final content).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A common misconception is that CSR means the server does nothing. It doesn't — Next.js still renders and sends a minimal HTML shell from the server for Client Components (usually the loading/empty state). What makes it &quot;client-side&quot; is that the &lt;strong&gt;data fetching and the final content render&lt;/strong&gt; happen in the browser, after JavaScript downloads and runs. The browser then &quot;hydrates&quot; that HTML — attaching event listeners and making it interactive — before fetching data and re-rendering with the real content.&lt;/p&gt;
&lt;p&gt;In Next.js, you get CSR by using Client Components with hooks like &lt;code&gt;useState&lt;/code&gt; and &lt;code&gt;useEffect&lt;/code&gt; (or a data-fetching library like React Query).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/live-notifications.tsx
&quot;use client&quot;;

import { useEffect, useState } from &quot;react&quot;;

type Notification = { id: string; message: string };

export function LiveNotifications() {
  const [notifications, setNotifications] = useState&amp;lt;Notification[]&amp;gt;([]);

  useEffect(() =&amp;gt; {
    fetch(&quot;/api/notifications&quot;)
      .then((res) =&amp;gt; res.json())
      .then(setNotifications);
  }, []);

  return (
    &amp;lt;ul&amp;gt;
      {notifications.map((n) =&amp;gt; (
        &amp;lt;li key={n.id}&amp;gt;{n.message}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the &lt;code&gt;&quot;use client&quot;&lt;/code&gt; directive tells Next.js this component should be interactive in the browser. The server sends an initial HTML shell (which may contain little or no real data), then the browser fetches data and renders the final UI after hydration. Once JavaScript loads and hydrates that HTML, &lt;code&gt;useEffect&lt;/code&gt; fires, fetches data from &lt;code&gt;/api/notifications&lt;/code&gt;, and re-renders the list with real content — all in the browser.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use CSR when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The content is highly interactive and user-specific (notification feeds, live chat, dashboards that update constantly).&lt;/li&gt;
&lt;li&gt;SEO doesn't matter for that specific piece of UI (search engines don't need to see it).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Making your &lt;em&gt;entire&lt;/em&gt; page a Client Component &quot;just to be safe.&quot; This kills SEO and slows down the initial load. Only mark the interactive parts as client components — let everything else stay on the server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-isr-incremental-static-regeneration-&quot;&gt;What Is ISR (Incremental Static Regeneration)?&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; a printed menu that quietly reprints itself in the background every hour — customers always get &lt;em&gt;a&lt;/em&gt; menu instantly, and it's rarely more than an hour out of date.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ISR is SSG's smarter sibling. Pages are still pre-built and served instantly like static pages, but Next.js automatically &lt;strong&gt;regenerates them in the background&lt;/strong&gt; after a set amount of time, without you needing to rebuild and redeploy your whole app.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 3600 }, // regenerate this page at most once every hour
  });
  return res.json();
}

export default async function ProductPage({
  params,
}: {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
}) {
  const { id } = await params;
  const product = await getProduct(id);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{product.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;${product.price}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;revalidate: 3600&lt;/code&gt; tells Next.js &quot;this page can be served from cache for up to 3,600 seconds (1 hour).&quot; The first visitor after that hour expires still gets the &lt;em&gt;old&lt;/em&gt; cached page instantly — but Next.js triggers a regeneration in the background, so the &lt;em&gt;next&lt;/em&gt; visitor gets the updated version. Nobody ever waits for a rebuild.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use ISR when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content changes occasionally, but not so often that it needs to be SSR (product pages, pricing pages, news listings).&lt;/li&gt;
&lt;li&gt;You want the speed of static pages without manually rebuilding every time data changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-are-cache-components-and-ppr-&quot;&gt;What Are Cache Components (and PPR)?&lt;/h3&gt;
&lt;p&gt;This is where things get genuinely exciting — and where most beginner tutorials stop short.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; a restaurant table that's already set, with bread on it the moment you sit down (static shell) — while your actual hot meal (dynamic content) is still being cooked and gets brought out the second it's ready.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cache Components&lt;/strong&gt; is Next.js's newer model for controlling caching explicitly, and it's what powers &lt;strong&gt;Partial Prerendering (PPR)&lt;/strong&gt; under the hood. Instead of choosing &lt;em&gt;one&lt;/em&gt; rendering strategy for an entire page, PPR lets a &lt;strong&gt;single page mix static and dynamic content together&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here's the key idea: Next.js pre-renders a &lt;strong&gt;static &quot;shell&quot;&lt;/strong&gt; of your page instantly (the parts that don't depend on the current request), and then &lt;strong&gt;streams in the dynamic parts&lt;/strong&gt; as soon as they're ready — all without making the visitor wait for the slowest piece of data.&lt;/p&gt;
&lt;h4 id=&quot;step-1-enable-cache-components&quot;&gt;Step 1: Enable Cache Components&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// next.config.ts
import type { NextConfig } from &quot;next&quot;;

const nextConfig: NextConfig = {
  cacheComponents: true, // turns on the Cache Components / PPR model
};

export default nextConfig;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Once &lt;code&gt;cacheComponents&lt;/code&gt; is enabled, Next.js stops caching things implicitly. You now have to explicitly mark what should be cached using &lt;code&gt;&quot;use cache&quot;&lt;/code&gt; — this makes caching predictable instead of &quot;magic.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-2-mark-cacheable-parts-with-use-cache-&quot;&gt;Step 2: Mark cacheable parts with &lt;code&gt;&quot;use cache&quot;&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/store/page.tsx
import { Suspense } from &quot;react&quot;;
import { CartStatus } from &quot;@/components/cart-status&quot;;

async function FeaturedProducts() {
  &quot;use cache&quot;; // this function's result gets cached and reused

  const res = await fetch(&quot;https://api.example.com/products/featured&quot;);
  const products = await res.json();

  return (
    &amp;lt;ul&amp;gt;
      {products.map((p: { id: string; name: string }) =&amp;gt; (
        &amp;lt;li key={p.id}&amp;gt;{p.name}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}

export default function StorePage() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Welcome to the Store&amp;lt;/h1&amp;gt;

      {/* Static shell — cached, renders instantly */}
      &amp;lt;FeaturedProducts /&amp;gt;

      {/* Dynamic hole — depends on the current user, streams in separately */}
      &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading your cart...&amp;lt;/p&amp;gt;}&amp;gt;
        &amp;lt;CartStatus /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;FeaturedProducts&lt;/code&gt; has &lt;code&gt;&quot;use cache&quot;&lt;/code&gt; at the top, so Next.js treats it as part of the static shell — it's pre-rendered and reused across visitors, just like SSG. &lt;code&gt;CartStatus&lt;/code&gt;, on the other hand, depends on &lt;em&gt;who's&lt;/em&gt; visiting (their cart), so it's wrapped in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;. Next.js sends the static shell (heading + featured products) &lt;strong&gt;instantly&lt;/strong&gt;, then streams in the cart status the moment it's ready — without blocking the rest of the page.&lt;/p&gt;
&lt;p&gt;This is the best of every world: the instant load of SSG, the freshness of SSR, all on the &lt;em&gt;same&lt;/em&gt; page, without you manually choosing one strategy for the whole route.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; You can control how long cached content lives using &lt;code&gt;cacheLife()&lt;/code&gt; (time-based expiry) and invalidate it on demand using &lt;code&gt;cacheTag()&lt;/code&gt; + &lt;code&gt;revalidateTag()&lt;/code&gt; — similar in spirit to ISR's &lt;code&gt;revalidate&lt;/code&gt;, but with much finer control over &lt;em&gt;what&lt;/em&gt; gets invalidated and &lt;em&gt;when&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Don't put &lt;code&gt;&quot;use cache&quot;&lt;/code&gt; on a function that returns data specific to the logged-in user with the default (shared) cache — that data could end up served to other users. Use &lt;code&gt;&quot;use cache: private&quot;&lt;/code&gt; for personalized cached content instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-comparison&quot;&gt;Quick Comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Strategy &lt;/th&gt;&lt;th&gt;When HTML is built &lt;/th&gt;&lt;th&gt;Best for &lt;/th&gt;&lt;th&gt;Server load per request &lt;/th&gt;&lt;th&gt;SEO&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SSR&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;On every request &lt;/td&gt;&lt;td&gt;Personalized, frequently changing data &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;Excellent&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SSG&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Once, at build time &lt;/td&gt;&lt;td&gt;Content that rarely changes &lt;/td&gt;&lt;td&gt;None &lt;/td&gt;&lt;td&gt;Excellent&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CSR&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Minimal shell on server, content rendered in browser after hydration &lt;/td&gt;&lt;td&gt;Highly interactive, non-SEO UI &lt;/td&gt;&lt;td&gt;Low (just the shell) &lt;/td&gt;&lt;td&gt;Depends&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ISR&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;At build time + periodic background refresh &lt;/td&gt;&lt;td&gt;Content that changes occasionally &lt;/td&gt;&lt;td&gt;Low &lt;/td&gt;&lt;td&gt;Excellent&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Cache Components (PPR)&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Mixed — static shell instantly, dynamic parts streamed &lt;/td&gt;&lt;td&gt;Pages with both static and personalized sections &lt;/td&gt;&lt;td&gt;Only for the dynamic parts &lt;/td&gt;&lt;td&gt;Excellent&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions-1&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;do-i-have-to-pick-one-strategy-for-my-whole-app-&quot;&gt;Do I have to pick one strategy for my whole app?&lt;/h4&gt;
&lt;p&gt;No — and that's the whole point of Cache Components. You can mix strategies &lt;em&gt;per page&lt;/em&gt;, and with PPR, even &lt;em&gt;within the same page&lt;/em&gt;. Most real-world Next.js apps use a combination of all of these depending on the route.&lt;/p&gt;
&lt;h4 id=&quot;is-isr-the-same-as-cache-components-&quot;&gt;Is ISR the same as Cache Components?&lt;/h4&gt;
&lt;p&gt;Not quite. ISR revalidates an entire page on a timer. Cache Components give you finer control — you choose exactly &lt;em&gt;which functions or components&lt;/em&gt; get cached, for how long, and how they get invalidated, all while letting other parts of the same page stay dynamic.&lt;/p&gt;
&lt;h4 id=&quot;will-cache-components-replace-isr-completely-&quot;&gt;Will Cache Components replace ISR completely?&lt;/h4&gt;
&lt;p&gt;For new projects using &lt;code&gt;cacheComponents: true&lt;/code&gt;, yes — &lt;code&gt;&quot;use cache&quot;&lt;/code&gt; with &lt;code&gt;cacheLife()&lt;/code&gt; covers what &lt;code&gt;revalidate&lt;/code&gt; used to do, plus more. Existing apps using the older caching model can keep working as-is, but new apps are encouraged to adopt Cache Components.&lt;/p&gt;
&lt;h4 id=&quot;does-ssr-hurt-my-seo-&quot;&gt;Does SSR hurt my SEO?&lt;/h4&gt;
&lt;p&gt;No — search engines see fully rendered HTML either way with SSR, SSG, or ISR. CSR is the one to watch, since content rendered purely in the browser can be harder for some crawlers to index reliably.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-know-which-strategy-a-page-is-using-&quot;&gt;How do I know which strategy a page is using?&lt;/h4&gt;
&lt;p&gt;Run &lt;code&gt;next build&lt;/code&gt; and check the build output — Next.js labels each route as Static, Dynamic, or Partial Prerendered (when Cache Components is enabled), so you can see exactly how each page will behave in production.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Always test caching behavior with &lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt; rather than &lt;code&gt;next dev&lt;/code&gt; — caching behaves differently in development mode and can be misleading.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up-1&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Rendering strategy isn't a one-time architectural decision anymore — it's a per-component choice. SSR gives you freshness, SSG gives you speed, CSR gives you interactivity, ISR gives you a balance between the two static options, and Cache Components (powered by PPR) let you combine all of it on a single page without compromise.&lt;/p&gt;
&lt;p&gt;Start simple: default to SSG/ISR for content-heavy pages, reach for SSR only when you genuinely need per-request freshness, and once you're comfortable, experiment with Cache Components to squeeze instant load times out of pages that still need personalized data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Decision Tree

Is content personalized?
→ SSR

Is content mostly static?
→ SSG

Does it update occasionally?
→ ISR

Is it highly interactive UI?
→ CSR

Need static + dynamic on same page?
→ Cache Components / PPR
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;useful-resources-1&quot;&gt;Useful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/functions/fetch&quot;&gt;Next.js Fetch API Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/pages/building-your-application/rendering/client-side-rendering&quot;&gt;Next.js CSR Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering&quot;&gt;Next.js SSR Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation&quot;&gt;Next.js SSG Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/getting-started/caching&quot;&gt;Next.js Cache Components Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;continue-learning-1&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/saas-starter-architecture-nextjs&quot;&gt;SaaS Starter Architecture with Next.js 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/sanity-nextjs-16-guide&quot;&gt;How to Integrate Sanity CMS with Next.js App Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;How to Add Clerk Authentication in Next.js 16 (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23657/next-js-rendering-strategies-explained-ssr-ssg-csr-isr-cache-components-2026</guid>
<pubDate>Tue, 28 Jul 2026 10:32:23 +0000</pubDate>
</item>
</channel>
</rss>