Skip to content
All Articles

How Next.js Renders an Application

Understand Server and Client Components, prerendering, streaming, caching, and dynamic rendering in the Next.js 16 App Router.

7 min read
  • Next.js
  • React
  • Rendering
On this page
  1. The useful parts of MPA and SPA architecture
  2. Server and Client Components
  3. Prerendered, cached, streamed, and dynamic content
  4. Static generation
  5. Incremental and on-demand revalidation
  6. Request-time rendering
  7. Streaming request-time content
  8. Component-level caching with use cache
  9. Choosing based on freshness and personalization
  10. A complete mental model
  11. Key takeaways
  12. What comes next
  13. Further reading

Next.js pages are often classified as static, server-rendered, or client-rendered.

That vocabulary is useful, but it is no longer enough to explain the App Router.

A single route can contain a prerendered shell, cached data, interactive Client Components, and request-time content streamed through a Suspense boundary.

The better question is: when and where does each part of the component tree do its work?

The useful parts of MPA and SPA architecture

In a traditional multi-page application, navigation requests a new HTML document from the server.

The browser receives content it can display immediately. But each navigation replaces the document, and the server commonly renders every page request.

In a single-page application, the browser first loads an application shell and JavaScript bundle. Client code then changes the interface and fetches data without replacing the document.

Navigation can feel fluid, but the browser may need more JavaScript and more work before useful content appears.

Next.js combines ideas from both models.

The server can send rendered HTML for the initial visit. After hydration, the client router can navigate without a full document reload while preserving interactive state where appropriate.

This hybrid behavior is not one rendering mode. It is the result of server rendering, React Server Components, Client Components, prerendering, caching, and client navigation working together.

Server and Client Components

App Router layouts and pages are Server Components by default.

A Server Component executes on the server. It can read from server-side data sources, use secrets, and render UI without adding its own component code to the browser's JavaScript bundle.

Use a Client Component when the component needs state, event handlers, effects, or browser-only APIs such as window and localStorage.

Add the directive at the client boundary:

"use client"

import { useState } from "react"

export function Quantity() {
  const [value, setValue] = useState(1)

  return <button onClick={() => setValue(value + 1)}>Quantity: {value}</button>
}

'use client' does not mean that the entire route becomes a client-rendered SPA.

It marks a boundary. The server can still render the route's initial result, while the browser receives the JavaScript required to hydrate that Client Component and its client imports.

Keep client boundaries narrow. A small interactive control rarely requires the whole page to become a Client Component.

Prerendered, cached, streamed, and dynamic content

Older explanations ask whether a whole page is SSG or SSR. Next.js 16 can make the decision at smaller boundaries.

With Cache Components enabled, Next.js can combine four kinds of work in one route:

  • Prerendered: output produced ahead of a user request
  • Cached: reusable output from a component or function
  • Streamed: request-time output sent when a Suspense boundary resolves
  • Dynamic: fresh work that depends on the incoming request or uncached data

These categories overlap. A cached component can be included in a prerendered shell, and a Client Component can receive HTML from server rendering before it hydrates.

How prerendered, cached, streamed, and dynamic rendering relate to Server and Client Components.

The unit of reasoning is the component tree, not only the URL.

Static generation

If a route can be completed without request-time information or uncached network data, Next.js can prerender it.

The build produces HTML for direct visits and a React Server Component payload for client navigation.

Documentation, marketing pages, and articles are natural candidates because many users can receive the same output.

Dynamic route parameters can also be produced ahead of time with generateStaticParams:

export async function generateStaticParams() {
  const posts = await getPosts()

  return posts.map((post) => ({ slug: post.slug }))
}

Prerendering removes request-time rendering work. It does not mean that the page contains no JavaScript or cannot include Client Components.

Incremental and on-demand revalidation

Some content is shared by every visitor but changes after deployment.

Incremental Static Regeneration lets Next.js reuse generated output and regenerate it after a configured revalidation period.

import { cacheLife } from "next/cache"

export async function Posts() {
  "use cache"
  cacheLife("hours")

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

With Cache Components, cacheLife controls time-based revalidation inside a 'use cache' scope. Time-based revalidation is useful when freshness can follow a predictable interval.

Without Cache Components, the previous App Router model uses options such as next.revalidate and the route-level revalidate export. Those route-segment options are disabled when Cache Components is on.

On-demand revalidation is more precise. After a mutation or webhook, revalidatePath can invalidate a route, while tagged data can be invalidated with revalidateTag.

"use server"

import { revalidatePath } from "next/cache"

export async function publishPost() {
  await savePost()
  revalidatePath("/posts")
}

Invalidation and regeneration are not the same moment. In the App Router, invalidating a path causes a later request to regenerate it.

ISR is also more than one Cache-Control header. Next.js tracks cached output and invalidation, while the deployment platform or self-hosted cache handler determines how that state is stored and shared.

Request-time rendering

Some output cannot be safely reused across requests.

A dashboard may depend on cookies. Search results may depend on searchParams. A stock level may need uncached data at the instant of the request.

That work belongs at request time.

import { cookies } from "next/headers"

export default async function AccountPage() {
  const session = (await cookies()).get("session")
  const account = await getAccount(session?.value)

  return <Account account={account} />
}

Request-time rendering is not a failure to optimize. It is the correct choice when the response depends on the current request or must be immediately fresh.

The performance goal is to isolate that work instead of forcing unrelated content to wait for it.

Streaming request-time content

React Suspense gives request-time work a boundary.

Next.js can send available UI first, show fallback content for the pending subtree, and stream the completed result later.

import { Suspense } from "react"

export default function ProductPage() {
  return (
    <>
      <ProductDescription />
      <Suspense fallback={<p>Loading availability…</p>}>
        <Availability />
      </Suspense>
    </>
  )
}

Streaming improves when useful content appears. It does not make the database query itself faster.

Place Suspense boundaries around meaningful regions. One boundary around the entire page delays everything; dozens of tiny boundaries can make the interface visually noisy.

Component-level caching with use cache

Cache Components is an opt-in Next.js 16 feature:

import type { NextConfig } from "next"

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

With it enabled, the 'use cache' directive can mark an async route, component, or function as cacheable.

import { cacheLife, cacheTag } from "next/cache"

export async function ProductDetails({ id }: { id: string }) {
  "use cache"
  cacheLife("hours")
  cacheTag(`product-${id}`)

  const product = await getProduct(id)
  return <article>{product.description}</article>
}

Next.js derives a cache key from the build, the function, and serializable arguments. Different product IDs therefore produce different cache entries.

Request APIs such as cookies() and headers() should normally be read outside a cached scope. Pass only the values that are genuinely part of the reusable result.

Cached output can sit inside a route that also contains request-time content.

For example, a product description can be cached for hours while a personalized basket is streamed for each request.

How a Next.js component tree maps to streamed response and browser work.

This is why a static-versus-dynamic label for the entire page can hide the most useful design choice.

Choosing based on freshness and personalization

Start with correctness, then reuse the largest safe region.

ContentRecommended starting point
Legal text or documentationPrerender it
Shared catalog data updated hourlyCache it with a lifetime or revalidation
Shared content changed by an editorCache it and invalidate on demand
User account detailsRender from request data
Live inventoryFetch uncached or use a very short, deliberate cache
Interactive filter controlMake the control a Client Component

Ask four questions for each subtree:

  1. Is the output identical for multiple users?
  2. How stale may it become?
  3. Does it require the incoming request?
  4. Does it require browser state or events?

The answers map naturally to prerendering, caching, request-time rendering, and Client Components.

Do not move a component to the client merely because its server work is dynamic. Dynamic server data and browser interactivity solve different problems.

A complete mental model

When Next.js prepares and serves an App Router route, picture this sequence:

  1. Server Components define the server-rendered tree.
  2. Client boundaries identify code that must also reach the browser.
  3. Prerenderable work becomes the initial static shell.
  4. 'use cache' scopes provide explicitly reusable results.
  5. Suspense boundaries defer uncached or request-dependent work.
  6. The server streams pending regions as they complete.
  7. The browser hydrates Client Components and enables interaction.

Not every route uses every step. The model lets one route combine them without pretending the whole page has only one rendering strategy.

Key takeaways

  • App Router pages and layouts are Server Components by default.
  • Client Components are for state, effects, events, and browser APIs.
  • Prerendering can produce a complete page or a partial static shell.
  • Suspense lets request-time content stream without blocking ready UI.
  • Cache Components makes caching explicit and available at route, component, or function scope.
  • ISR supports reusable generated output with time-based or on-demand revalidation.
  • Freshness, personalization, and interactivity should be decided independently for each subtree.

What comes next

We now know when Next.js can prepare, reuse, or defer UI work.

The final article follows those decisions through deployment: what next build produces, how a platform maps the output to infrastructure, and what path a production request takes.

Further reading