Skip to main content

Search

Search problems, hooks, and pages

Rendering Strategies

13 of 13

Key Points

  • Static (default): route rendered at build time when no runtime APIs are accessed. Served instantly from CDN.
  • Dynamic: automatically opted in when accessing cookies(), headers(), searchParams, or uncached data.
  • In Next.js 15, fetch() is NOT cached by default , but a route can still be static if no runtime APIs are used.
  • export const dynamic = "force-dynamic" , always renders dynamically, every request hits the server.
  • export const dynamic = "force-static" , forces static even if runtime APIs are accessed (they return empty).
  • generateStaticParams: pre-generate known dynamic paths statically; unknown paths fall back to dynamic.
// Force static
export const dynamic = 'force-static';

// Force dynamic
export const dynamic = 'force-dynamic';

// These automatically opt into dynamic rendering:
const cookieStore = await cookies();   // ← makes route dynamic
const headersList = await headers();   // ← makes route dynamic
const { q } = await searchParams;     // ← makes route dynamic

Warning

Gotcha

Calling cookies() or headers() anywhere in a Server Component automatically opts the entire page into dynamic rendering , even if the result isn't used. Extract them as close to the consuming component as possible.

Key Points

  • Add "use cache" to a function or component , its output is baked into the static shell at build time.
  • generateStaticParams() declares which dynamic paths (e.g., blog slugs) are pre-generated at build time.
  • Paths NOT in generateStaticParams render dynamically on first request, then cached.
  • cacheLife("days") or cacheLife("weeks") controls how long the cached output is valid.
  • Benefits: instant load times, global CDN distribution, no server involved per request.
async function BlogPosts() {
  'use cache';
  cacheLife('days');        // regenerate after 24 hours

  const posts = await fetchPosts();
  return <PostList posts={posts} />;
}

// Pre-generate /blog/[slug] at build time
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

Key Points

  • A route is dynamically rendered when it uses: cookies(), headers(), or searchParams.
  • fetch() without "use cache" runs fresh on every request , implicitly dynamic.
  • Dynamic rendering guarantees up-to-date data but adds server latency per request.
  • Even fully dynamic pages benefit from streaming , the static shell (layout) is sent to the browser immediately.
  • Combine static and dynamic on the same page using Suspense: static shell instant, dynamic parts stream in.
// Dynamically rendered , per-request, personalized
export default async function ProfilePage() {
  // cookies() opts the whole page into dynamic rendering
  const userId = (await cookies()).get('userId')?.value;
  const profile = await fetchProfile(userId);  // fresh every request

  return <Profile data={profile} />;
}

Key Points

  • Time-based: cacheLife("hours") , cached for that duration, then revalidated in the background.
  • On-demand: cacheTag("posts") marks cached content; updateTag("posts") in a Server Action instantly expires it.
  • Stale-while-revalidate: serve the cached page immediately while revalidating in the background.
  • Legacy API: export const revalidate = 60 (seconds) or fetch(url, { next: { revalidate: 60 } }) still work.
  • Per-fetch ISR: cache individual fetches independently from the page-level cache.

Note

Textbook Definition

ISR lets you update statically generated pages after the initial build. Next.js 15 uses "use cache" with cacheLife() for time-based ISR and cacheTag() + updateTag() for on-demand invalidation.
// Time-based ISR
async function Products() {
  'use cache';
  cacheLife('hours');           // revalidate every hour
  const products = await db.products.findAll();
  return <ProductGrid products={products} />;
}

// On-demand ISR via Server Action
async function publishProduct(data) {
  'use server';
  await db.products.create(data);
  updateTag('products');        // immediately bust the cache
}

Warning

Gotcha

On-demand revalidation (updateTag) only invalidates the cache on the server instance that handled the request. In multi-instance/serverless deployments, use a shared cache store ('use cache: remote') to propagate invalidation everywhere.

Key Points

  • Add loading.tsx in any route folder , Next.js automatically wraps page.tsx in <Suspense fallback={<Loading />}>.
  • The layout renders and is sent immediately; loading.tsx shows while the page awaits data.
  • User sees layout + loading state instantly on navigation , content swaps in when ready.
  • loading.tsx is a Server Component , render skeletons, spinners, or meaningful partial UI.
  • Scoped per segment: blog/loading.tsx only applies to /blog, not to /blog/[slug].
// app/dashboard/loading.tsx
// → automatically wraps app/dashboard/page.tsx in <Suspense>
export default function Loading() {
  return (
    <div className="space-y-4">
      <Skeleton className="h-8 w-64" />
      <Skeleton className="h-4 w-full" />
      <Skeleton className="h-4 w-3/4" />
    </div>
  );
}
Streaming with Suspenseintermediatestreaming
Wrap slow async components in <Suspense> to send the page shell instantly and stream the slow parts in when ready.

Key Points

  • <Suspense fallback={<Skeleton />}> shows the fallback until the async child resolves.
  • Everything outside Suspense boundaries is part of the static shell , sent immediately.
  • Multiple independent Suspense boundaries stream in parallel , one slow request never blocks others.
  • React's use() hook: pass a server-initiated Promise to a Client Component and let Suspense handle the wait.
  • loading.tsx is a shortcut , it's equivalent to wrapping page.tsx in a <Suspense> boundary.

Note

Textbook Definition

Streaming uses HTTP chunked transfer to progressively deliver HTML from server to client. React's Suspense boundaries define what can be deferred , the static shell arrives instantly while data-dependent chunks stream in as they resolve.
export default function Page() {
  return (
    <>
      <StaticHeader />                  {/* sent immediately */}

      <Suspense fallback={<PostsSkeleton />}>
        <BlogPosts />                   {/* streams in independently */}
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />                    {/* streams in independently */}
      </Suspense>
    </>
  );
}

Warning

Gotcha

Nesting async components without Suspense creates a waterfall , if the parent awaits before rendering the child, the child's fetch can't start until the parent finishes. Wrap each independently-fetching component in its own Suspense.

Key Points

  • "use cache" inside a function/component body caches its async output at data-level or UI-level.
  • cacheLife("seconds" | "minutes" | "hours" | "days" | "weeks") , sets expiry duration.
  • cacheTag("key") marks a cache entry; updateTag("key") from a Server Action invalidates it on demand.
  • Arguments become cache keys automatically , different inputs → separate cache entries.
  • Replaces the old { next: { revalidate } } fetch option with a composable, component-level model.

Note

Textbook Definition

"use cache" is a Next.js directive (requires cacheComponents: true in next.config.ts) that stores the return value of an async function or component. Function arguments automatically become part of the cache key, enabling parameterized caching.
import { cacheLife, cacheTag } from 'next/cache';

// Cache an individual data fetch
async function getProduct(id: string) {
  'use cache';
  cacheLife('hours');
  cacheTag(`product-${id}`);
  return db.products.findById(id);
}

// On-demand invalidation (Server Action)
async function updateProduct(id: string, data) {
  'use server';
  await db.products.update(id, data);
  updateTag(`product-${id}`);  // only this product's cache busted
}

Warning

Gotcha

"use cache" requires cacheComponents: true in next.config.ts , without it, the directive is silently ignored. This is the most common reason caching appears not to work.

Key Points

  • No extra configuration with Cache Components , PPR is the default rendering model.
  • Static shell: everything outside dynamic Suspense boundaries pre-rendered at build time.
  • Dynamic slots: Suspense fallback HTML is included in the static shell; real content streams at request time.
  • Same URL can serve static nav, cached blog posts, AND per-user recommendations.
  • Achieves TTFB as fast as pure static hosting while supporting fully dynamic personalized sections.

Note

Textbook Definition

PPR combines static and dynamic rendering on a single page without compromise. At build time, Next.js pre-renders everything that can be static (cached + deterministic) into an HTML shell. At request time, dynamic Suspense boundaries stream in personalized content.
// One page , three rendering modes simultaneously
export default function ProductPage() {
  return (
    <>
      {/* Static — pre-rendered at build, served from CDN */}
      <StaticNav />

      {/* Cached — in static shell, revalidates periodically */}
      <CachedReviews />

      {/* Dynamic — streams in per-request (personalized) */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <PersonalizedRecommendations />
      </Suspense>
    </>
  );
}

Key Points

  • CSR: the browser downloads JS, then renders , first paint is an empty shell (poor SEO).
  • Pre-rendering: Next.js generates HTML ahead of time so content is visible immediately.
  • Two pre-render modes: SSG (at build time) and SSR (per request).
  • After HTML arrives, React HYDRATES it , attaching event listeners to make it interactive.
  • Pre-rendering improves first contentful paint, SEO, and perceived performance.
// CSR: <div id="root"></div> → JS fills it in the browser
// SSR/SSG: server sends full HTML → React hydrates it

// Pre-rendered page is visible (and crawlable) before JS loads

Key Points

  • During build, Next.js analyzes each Pages-Router page for data dependencies.
  • A page WITHOUT getServerSideProps / getInitialProps is auto-optimized to static HTML.
  • Such pages serve instantly with no per-request server render.
  • Pages using only client state, context, or client libraries qualify.
  • For freshness, combine with ISR (revalidate) so static pages update over time.
// Auto-statically-optimized , no data functions
export default function About() {
  return <h1>About us</h1>; // prerendered to static HTML at build
}

// Opts OUT of static optimization → SSR every request
export async function getServerSideProps() {
  return { props: {} };
}

Warning

Gotcha

Adding getServerSideProps (even returning nothing useful) opts a page OUT of static optimization, forcing SSR on every request. Only add it when you truly need per-request data.

Key Points

  • Runs ONCE at build time (or during ISR revalidation) , never on the client.
  • Returns { props } that are passed to the page component.
  • Add revalidate: n to enable ISR , the page regenerates at most every n seconds.
  • Ideal for content that is the same for all users (blogs, docs, marketing).
  • Pair with getStaticPaths for dynamic routes ([id]).
export async function getStaticProps() {
  const data = await fetchData();
  return {
    props: { data },
    revalidate: 60   // ISR: regenerate at most once per minute
  };
}

export default function Page({ data }) {
  return <List items={data} />;
}

Warning

Gotcha

getStaticProps runs only on the server at build , it is stripped from the client bundle, so you can safely use secrets and direct DB calls inside it.

Key Points

  • Runs on EVERY request , always fresh, can read req/res, cookies, headers.
  • Returns { props } computed server-side and passed to the page.
  • Use for personalized or frequently-changing data (dashboards, authed pages).
  • Slower than SSG: adds server latency per request (no CDN caching of HTML).
  • Can return { redirect } or { notFound: true } to control the response.
export async function getServerSideProps(context) {
  const { req, params } = context;
  const user = await getUser(req.cookies.token);
  if (!user) return { redirect: { destination: '/login', permanent: false } };
  return { props: { user } };  // fresh on every request
}

Warning

Gotcha

getServerSideProps disables static optimization for that route. If the data does not actually change per request, use getStaticProps + ISR instead for far better performance.

Key Points

  • Used with getStaticProps for dynamic routes (pages/blog/[id].js).
  • Returns { paths, fallback } , paths is the list to pre-render at build time.
  • fallback: false , any path not listed returns a 404.
  • fallback: true , serve a fallback page, then generate in the background and cache.
  • fallback: 'blocking' , SSR the page on first request, then cache it (no fallback UI).
  • App Router equivalent: generateStaticParams (no fallback option , uses dynamicParams).
export async function getStaticPaths() {
  const posts = await getPosts();
  return {
    paths: posts.map((p) => ({ params: { id: p.id } })),
    fallback: 'blocking'   // un-listed ids: SSR once, then cache
  };
}

export async function getStaticProps({ params }) {
  const post = await getPost(params.id);
  return { props: { post } };
}

Warning

Gotcha

With fallback: true the page first renders with no data , you must handle router.isFallback and show a loading state, or it crashes accessing undefined props.