
The React Server Components model is the part of the Next.js App Router that trips up experienced React developers the most, because it inverts a default they've held for years. In the App Router, every component is a Server Component unless you say otherwise. The 'use client' directive is the opt-in to the browser, not the other way around. Once that clicks, most of the confusion dissolves.
What "runs where" actually means#
A Server Component runs once, at request or build time, on the server. Its code never ships to the browser. It can read the filesystem, hit a database directly, or await data inline — and the client receives only the rendered output, not the component's JavaScript.
A Client Component is what you already know: it's compiled into the bundle, hydrated in the browser, and can use state, effects, and event handlers. The catch is that everything imported into a Client Component becomes part of the client bundle too.
That asymmetry is the whole game. Server Components are free, weight-wise; Client Components cost bytes the user downloads and executes. This is the same main-thread budget that governs your Core Web Vitals — every kilobyte of client JavaScript is parsed and run on the thread that also has to answer user input.
The rule of thumb#
Keep components on the server by default. Reach for 'use client' only when a component needs one of:
- Interactivity —
onClick,onChange, and other event handlers. - State or lifecycle —
useState,useReducer,useEffect. - Browser-only APIs —
window,localStorage,IntersectionObserver.
If a component does none of those, it has no reason to be a Client Component. A static article body, a product card, a footer — server. This is exactly why this site's blog renders almost entirely on the server, with only a handful of small interactive islands (search, the table-of-contents scroll-spy, copy-able code blocks) marked 'use client'.
Push the boundary down, not up#
The most common mistake is marking a big layout component 'use client' because one leaf inside it needs a click handler. That drags the entire subtree — and its imports — into the bundle.
The fix is to push the 'use client' boundary as far down the tree as possible. Keep the layout on the server and isolate the interactive bit into its own small Client Component:
// page.tsx — stays a Server Component
import { LikeButton } from './LikeButton'; // the only client island
export default async function Page() {
const post = await getPost(); // server-side data, no client JS
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton postId={post.id} />
</article>
);
}
// LikeButton.tsx
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(v => !v)}>{liked ? '♥' : '♡'}</button>;
}
Only LikeButton and its dependencies ship to the browser. The article shell stays weightless. This "islands" approach is one of the highest-leverage moves in the broader toolkit of Next.js code optimization.
Passing data across the boundary#
A Server Component can render a Client Component and pass it props — but those props must be serializable. Strings, numbers, plain objects, and arrays cross fine; functions and class instances do not. You can also pass Server Components as children to a Client Component, which lets a client wrapper (say, a theme provider or an animation container) enclose server-rendered content without forcing that content onto the client.
One thing that catches people: a Client Component can't await server data directly. Fetch it on the server and pass it down as a prop, or fetch it in the browser with something like TanStack Query. And if you're managing genuinely client-side effects, the timing rules still apply — a mistimed layout read is a mistimed layout read whether you're in the Pages Router or the App Router, as covered in useEffect vs useLayoutEffect.
The takeaway#
Server by default, client on purpose. Ask of every component, "does this need the browser?" — and if not, leave it on the server, where it costs nothing to ship. Push each 'use client' boundary down to the smallest leaf that truly needs it, and your bundle stays lean without any heroics. If you want to see the pattern applied end-to-end, several of my frontend projects lean on it, and I'm always happy to talk shop.
FAQ#
Are Server Components the default in Next.js?#
Yes. In the Next.js App Router, every component is a React Server Component unless the file (or one it's imported from) starts with the 'use client' directive. 'use client' is the opt-in to the browser, not the reverse.
When should I use 'use client'?#
Add 'use client' only when a component needs interactivity (event handlers), state or lifecycle hooks (useState, useEffect), or browser-only APIs (window, localStorage). Components that just render data should stay on the server.
Why does pushing the 'use client' boundary down matter?#
Everything imported into a Client Component ships to the browser. If you mark a large layout as a client component just for one interactive leaf, the whole subtree and its dependencies get bundled. Isolating the interactive part into a small client island keeps the rest server-rendered and weightless.
Can a Client Component fetch server data directly?#
No — a Client Component can't await server-side data. Fetch it in a Server Component and pass it down as serializable props, or fetch it in the browser with a client data library. You can also pass Server Components as children into a Client Component wrapper.
References: