Dynamic vs Static: Controlling the Boundary

Know exactly what tips a route into dynamic rendering, and use the route segment config to steer it deliberately.

Every route resolves to static (rendered once, cached, reused) or dynamic (rendered per request). Next infers this — but a senior engineer decides it on purpose and knows which lines of code flip the switch.

What forces dynamic

  • Reading request-bound APIs: cookies(), headers(), draftMode(), or the searchParams prop.
  • A fetch with cache: 'no-store'.
  • Exporting export const dynamic = 'force-dynamic'.
  • Using the uncached function connection() (Next 15) to explicitly opt out.

If none of these appear, the route is static and blazing fast.

The route segment config knobs

ExportEffect
dynamic = 'force-static'force static; dynamic APIs return empty/default
dynamic = 'force-dynamic'force per-request rendering
revalidate = 60ISR window for the whole route
fetchCacheoverride default fetch caching
dynamicParams = false404 params not returned by generateStaticParams

The key insight

Dynamic is not the enemy — accidental dynamic is. A stray cookies() read high in a shared layout can silently turn an entire subtree dynamic and quietly kill your static performance. Keep per-request reads low and narrow, and let Partial Prerendering (next lesson) give you the best of both.

Example

Example · typescript
// Deliberately static, revalidated hourly — even though the API
// is dynamic-capable, we pin the behavior explicitly.
export const dynamic = 'force-static';
export const revalidate = 3600;

export default async function MarketingStats() {
  const res = await fetch('https://api.example.com/stats');
  const data = await res.json();
  return <p>Trusted by {data.customers} teams</p>;
}

// Contrast: this route is intentionally dynamic because
// it personalizes on a per-request cookie.
// export const dynamic = 'force-dynamic';
// import { cookies } from 'next/headers';
// const theme = (await cookies()).get('theme')?.value;

When to use it

  • A developer audits which routes are dynamic by running next build and examining the route type column in the output.
  • A team wraps the personalised header in a Client Component behind Suspense so the static page body still pre-renders.
  • An engineer uses unstable_noStore() inside a utility function to force a specific data call to bypass the cache without changing the route.

More examples

What makes a route dynamic

Calling cookies(), headers(), or using cache:'no-store' signals to Next.js that the route cannot be pre-rendered.

Example · ts
import { cookies, headers } from 'next/headers';

// Any of these in a page cause dynamic rendering:
const cookieStore = await cookies();       // reads request cookies
const reqHeaders = await headers();        // reads request headers
const res = await fetch(url, { cache: 'no-store' }); // uncached fetch

Keep static body, dynamic header

Suspense isolates the dynamic UserGreeting inside a boundary, letting the static article body pre-render as HTML.

Example · ts
import { Suspense } from 'react';
import UserGreeting from './UserGreeting'; // reads cookies

export default function Page() {
  return (
    <>
      <Suspense fallback={<div className="h-10" />}>
        <UserGreeting />
      </Suspense>
      <article>Static article content rendered at build time</article>
    </>
  );
}

unstable_noStore for utility functions

unstable_noStore() inside a helper marks only that call as uncached, rather than forcing the whole route to be dynamic.

Example · ts
import { unstable_noStore as noStore } from 'next/cache';

export async function getRandomQuote() {
  noStore(); // opt this call out of caching without changing the route
  const res = await fetch('https://api.quotable.io/random');
  return res.json();
}

Discussion

  • Be the first to comment on this lesson.