Skip to content
lazy devs
3 min readLazy Devs

A practical mental model for App Router rendering

Server Components, client islands, and the static-to-dynamic spectrum. A field guide to where your code actually runs in the Next.js App Router.

The App Router confuses people because the same file can run in three different places: at build time, on the server per request, or in the browser. Get a clear model for which is which and you can reason about performance instead of guessing. Stay fuzzy on it and you end up with a slow site, a giant client bundle, and a "why is this re-rendering" bug you cannot explain.

Here is the model we actually use day to day.

Everything is a Server Component until you opt out

In the App Router, components render on the server by default. They produce HTML, ship zero JavaScript for themselves, and can read data directly without an API layer in between. You opt a component into the browser with a single directive:

"use client";
 
import { useState } from "react";
 
export function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

The rule we follow: keep "use client" on the smallest possible leaf. A button that needs state becomes a client component. The page that renders it does not. This is the single most important habit for keeping a Next.js app fast, and it is the foundation of every web app we build.

The common mistake is putting "use client" at the top of a page or layout because something deep inside needs interactivity. That opts the entire subtree out of server rendering and ships all of it to the browser. Push the directive down to the leaf instead, and let the rest stay on the server.

Server and client are not either/or

Server Components can render client components, and client components can receive server-rendered children through props. So a static article (server) can contain a small interactive widget (client) without becoming a client component itself. You compose them. This is what lets you keep 95% of a page as zero-JavaScript HTML while still having the one piece that needs to move.

If you only remember one diagram, make it this: the server renders the page and hands the browser a small set of interactive islands floating in a sea of static HTML. For the deeper version of how data flows through those server components, see our notes on data fetching in React Server Components.

The static-to-dynamic spectrum

A route is static when nothing forces it to run per request. Reach for cookies(), headers(), searchParams, or an uncached fetch, and the route becomes dynamic, rendered fresh on every request. For a marketing site or a content-heavy app, you want every route static:

  • No per-request APIs in the render path.
  • Data read at build time from local files or a cached source.
  • generateStaticParams for dynamic segments so each one is prerendered.
export function generateStaticParams() {
  return posts.map((post) => ({ slug: post.slug }));
}
 
export const dynamicParams = false; // unknown slugs 404 at build, not runtime

When next build finishes, the output report tells you the truth. A circle means the route is static; a different marker means it renders on demand. If a page you expected to be static shows up dynamic, something on it reached for a per-request API. Track it down rather than shrugging, because that one accidental cookies() call can turn a whole section dynamic.

Caching is the other half

Rendering decides where your code runs. Caching decides how often. The two interact constantly, and most "stale data" and "why is this slow" issues live at that seam. It is worth understanding both together, which is why we wrote a separate guide on Next.js caching. The short version: static routes are cached aggressively by default, and you opt into freshness deliberately rather than the other way around.

Why this matters for performance

Static routes have no cold start and no render cost on the request path. Client islands stay tiny, so the main thread is free and interaction stays responsive. You get the interactivity you need without paying for hydration you do not.

There is a real cost ceiling here too. Every kilobyte you keep on the server is a kilobyte the browser never downloads, parses or executes. On mobile, where CPU is the bottleneck, that is the difference between a page that feels instant and one that janks on first tap.

The takeaway

Keep the server doing the heavy lifting. Put "use client" only on the leaves that genuinely need it. Keep routes static unless a real per-request need forces otherwise, and watch the build output to confirm it. Do that and the App Router rewards you with a fast site that is also pleasant to build. If you want a second pair of hands on a Next.js build or rescue, that is exactly the kind of work we do at Lazy Devs.

Related service

Next.js Development Services

Production Next.js apps and sites, tuned for speed and search.

Learn more

Want this built right?

This is the work we do every day. Tell us what you are building and we will show you exactly how we would ship it.

hello@lazydevsagency.com