Server vs. Client Components in 2026 · the rule we apply
Two years into the RSC era, the decision tree got simpler. Here is the one we apply, with the wrong answers we already paid for.
Two years into the RSC era, the decision tree got simpler. Here is the one we apply, with the wrong answers we already paid for.
Two years into shipping React Server Components in production, the gut feel finally stabilised. Most teams we work with still treat the choice as religious · 'we are an RSC shop' or 'just put `'use client'` on top, it works'. Both lose money. The actual rule is shorter than either camp wants to admit.
We default every component to a Server Component, and we only promote it to a Client Component when one of four triggers fires. That is the whole rule. The rest of this post is the four triggers, the cost we pay for getting it wrong, and the cases where the right answer is 'neither, refactor the boundary'.
If a component renders data, lays out content, composes other components, or calls a database, it is a Server Component. No state, no `useEffect`, no event handlers · just `async` functions and JSX. We do not put `'use client'` on a card, a list, a layout, or a section header by reflex anymore. That habit was leftover from the Pages Router years and it ships JS for nothing.
Anything else is a server component. If you are unsure between '4' and 'this is just a link', the answer is server · a link is `<a href>`, not an `onClick` handler.
A component that should have been server, marked client: extra JS to download, parse, hydrate. Every prop crossing the boundary becomes serialisation cost. Suddenly your nice composable tree carries a JSON blob that flows down with the HTML. Two extra `'use client'` files in a layout is how a marketing page loses 40KB of bundle for no gain.
A component that should have been client, kept on the server: it does not interact, the user clicks and nothing happens, or the dev wraps the parent in `'use client'` to fix it. That last move is the most expensive · now the entire subtree, including its server-only children, has to be reauthored.
The thing nobody tells you on day one: the boundary between server and client is a real architectural artefact. You design it, you name it, you keep it small. We name our client islands after their job · `<TabsClient>`, `<EditorClient>`, `<MapClient>`. The server wrapper next to them stays composable and data-fetching. Anything else is mush.
// app/dashboard/page.tsx · server
import { TabsClient } from "./tabs-client";
import { fetchSummary, fetchActivity } from "@/lib/data";
export default async function Page() {
const [summary, activity] = await Promise.all([
fetchSummary(),
fetchActivity(),
]);
return (
<section>
<h1>Dashboard</h1>
<TabsClient
// server-rendered children pre-fill the tabs
summary={<SummaryView data={summary} />}
activity={<ActivityList items={activity} />}
/>
</section>
);
}
// app/dashboard/tabs-client.tsx
'use client';
import { useState, type ReactNode } from "react";
export function TabsClient({ summary, activity }: { summary: ReactNode; activity: ReactNode }) {
const [tab, setTab] = useState<"summary" | "activity">("summary");
return (
<>
<nav role="tablist">
<button onClick={() => setTab("summary")}>Summary</button>
<button onClick={() => setTab("activity")}>Activity</button>
</nav>
{tab === "summary" ? summary : activity}
</>
);
}
Notice what is in the client file: state, two buttons, a switch. That is it. The rendered children of each tab are server components passed in as props. We do not move data fetching across the boundary, we move slots.
Fetch on the server, then hand the result to a small client island. Best for filters, tabs, drawers, accordions. Keep the interactive surface tiny.
Form is a server component. Submit is a server action. Optimistic UI lives in a small client wrapper around the form. The number of `'use client'` directives stays at one.
Map, chart, rich-text editor: dynamically import inside a client component. Wrap with `<Suspense>` so the rest of the page does not wait. The server still renders the surrounding chrome.
If you find yourself fighting the boundary, you have probably colocated two things that should be siblings. Split the tree. The pattern: a server component that fetches and lays out, plus a sibling client component that handles the interaction, with `children` or render-prop slots between them. After enough of these, you stop reaching for `'use client'` defensively.
Check your bundle. If a marketing-style page ships more than 80KB of client JS, something is wearing `'use client'` that should not be. Ten minutes with the bundle analyzer almost always finds it.
The summary fits on a Post-it: server by default, promote on a real trigger, keep the client island small, design the boundary like you would design a public API. Two years in, we still mostly delete `'use client'` directives, not add them.

Founder, DField Solutions
I've shipped production products from fintech to creator-tooling · for startups and enterprises, from Budapest to San Francisco.
Let's talk about your project. 30 minutes, no strings.