All writing
August 23, 20264 min read

Server Components: Where to Draw the Client Boundary

The App Router argument is over. Where the server and client boundary sits is not and it has an answer per component rather than a general one.

The App Router has been the default for long enough that the argument about whether to use it is over. What is still unsettled is where the boundary between server and client should sit, and that question does not have a general answer. It has an answer per component.

Sixteen products later, here is how I decide, and where I have got it wrong.

The default is server, and the reason is weight

A Server Component sends HTML. A Client Component sends HTML plus the JavaScript needed to make it interactive plus the data it needs, serialised. That second one is not worse, it is just more expensive, and the cost lands on someone holding a phone on a train.

So the question is never "can this be a Client Component". It is "does this need to be". Interactivity, browser APIs, state that survives a click. Everything else stays on the server by default and the bundle stays small without anyone having to police it.

The boundary moves down, not up

The mistake I made repeatedly in the first year was marking a page as a Client Component because one thing inside it needed a click handler. That is the cheapest fix in the moment and the most expensive one later, because everything nested under it becomes client too.

The better move is to push the boundary as deep as it goes. The page stays on the server, the layout stays on the server, and the one button that needs an onClick is its own Client Component with nothing under it. Server Components can render Client Components, so a mostly static page with three interactive islands costs you three islands rather than a page.

Data goes down as props, not through a fetch on the client

Once the boundary is deep, a pattern falls out of it: fetch on the server, pass the result down.

This removes an entire category of work. No loading state for data you already had before rendering. No effect that runs after mount to go and get something. No waterfall where the component renders, then asks, then renders again.

Where I still reach for client fetching is genuinely dynamic data: something polling, something the user filters rapidly, something that updates over a socket. That is a real category and it is smaller than it first appears.

Server Actions removed more code than I expected

A form used to mean an API route, a fetch, a loading flag, error state, and a manual refresh of whatever the mutation changed. A Server Action collapses that into a function that runs on the server and a revalidation call.

Two things to keep in mind. It is a public endpoint no matter how it looks in your editor, so it needs the same authorization as any route: the fact that only your form calls it today is not a security property. And revalidation is the part people forget, because the mutation succeeds while the page keeps showing the old data, which reads as a bug in the write rather than a missing cache invalidation.

Where it genuinely hurts

Third party components. Anything expecting a browser at import time has to be isolated behind a client boundary, and some libraries make that harder than it should be. This is the tax, and it is real.

Debugging is also harder than it was. An error can now come from a render on the server, a render on the client, or the serialisation between them, and the stack trace does not always make the difference obvious. Rendering the same component in two places sounds like a small change and it is not.

The rule I would give

Start on the server. Move to the client only when something forces you, and when it does, move the smallest possible piece.

It sounds like a performance guideline and it is really a maintenance one. A page where the client boundary is drawn tightly is a page where you can see, from the file tree, exactly which parts have to work in a browser. That is worth more six months later than any bundle size number.

Frequently asked questions

Should a page be a Client Component if one button needs onClick?

No. That marks everything nested under it as client too. Keep the page and layout on the server and make the button its own Client Component. Server Components can render Client Components, so three interactive islands cost three islands rather than a page.

Do Server Actions need their own authorization?

Yes. A Server Action is a public endpoint no matter how it looks in your editor. The fact that only your form calls it today is not a security property, so it needs the same checks as any route.

When is client side fetching still the right call?

For genuinely dynamic data: something polling, something the user filters rapidly, something arriving over a socket. That category is real and smaller than it first appears. Everything else is better fetched on the server and passed down as props.