All Articles
Frontend Architecture & Best Practices

Server Components vs Client Components in Next.js: When to Use Each

The App Router made every component a decision. Server by default, client when you need interactivity - but the line is blurrier than the docs suggest. Here is a practical model for deciding without guessing.

Velox Studio11 min read

The Next.js App Router turned a question most React developers never asked into one they now face on every component: does this run on the server or the client? Server Components are the default. Client Components are opt-in with a directive. Simple in theory. In practice, the boundary between them is where most Next.js confusion lives.

Get the split right and your app is fast, your bundles are small, and your data flows cleanly. Get it wrong and you either ship interactivity that does not work or drag half your dependency tree into the browser for no reason. Here is a practical model for deciding, built from real production work rather than the happy-path examples in the docs.

The One-Sentence Version

Render on the server by default. Reach for a Client Component only when the component needs interactivity, browser APIs, or React state and effects. Keep the client boundary as low in the tree as you can.

That sentence is correct and also insufficient, because the interesting cases are the ones where it is not obvious which side a component belongs on. Let us get concrete.

What Server Components Actually Give You

A Server Component runs on the server, sends HTML to the browser, and ships zero JavaScript for itself. That is the headline. The practical consequences are larger than they first appear.

You can fetch data directly inside the component - no loading state, no client-side fetch, no waterfall. You can reach the database, read secrets, and call internal services without exposing any of it to the browser. Your bundle shrinks because the component's code never reaches the client. And the user sees content sooner because it arrives as HTML rather than as a blank page waiting for JavaScript to hydrate.

For anything that is fundamentally about displaying data - a product page, an article, a dashboard that reads and shows - Server Components are the correct default and a genuine upgrade. This connects to the broader data patterns we covered in Next.js data fetching strategy.

What Forces a Component to the Client

A component must be a Client Component the moment it needs any of the following: React state (useState, useReducer), lifecycle or side effects (useEffect), event handlers (onClick, onChange, and friends), browser-only APIs (window, localStorage, geolocation), or any React hook that depends on those.

You mark it with "use client" at the top of the file. That directive does not just affect that file - it marks the boundary. Everything imported into a Client Component that is also part of the render tree becomes client code too. This is the detail people miss, and it is where bundles quietly bloat.

The Mistake Almost Everyone Makes First

The instinct carried over from the old React world is to put "use client" at the top of the page and be done with it. It works. Everything is interactive. And you have thrown away most of what the App Router offered, because now the entire page renders on the client and ships its whole dependency tree to the browser.

The better pattern is the opposite. Keep the page a Server Component. Push "use client" down to the smallest leaf that actually needs it - the button, the form, the interactive widget - and leave everything around it on the server. A page can be a Server Component that renders a mostly-server tree with small islands of client interactivity. That is the architecture the App Router is designed for.

If you find yourself marking a large layout component as client because one button inside it needs an onClick, extract the button. This is the same instinct that keeps React apps fast - only ship to the browser what the browser genuinely needs.

The Composition Trick That Unlocks It

Here is the pattern that resolves most of the awkward cases: a Client Component can render Server Components passed to it as children or as props.

This matters because it lets you put an interactive shell around server-rendered content without dragging that content to the client. A client-side tab switcher, accordion, or modal can wrap server-rendered panels. The interactivity is client-side; the content inside stays on the server. You get the interaction you need without paying the bundle cost for everything it surrounds.

Once this clicks, a lot of the "but I need this to be interactive and also fetch data" problems dissolve. The interactive wrapper is a Client Component. The data-fetching content it wraps is a Server Component passed in as children. Clean boundary, both concerns handled.

A Practical Decision Checklist

When you are staring at a new component, run these questions in order.

Does it use state, effects, event handlers, or browser APIs? If yes, it is a Client Component. If no, keep going.

Does it fetch data or read server-only resources? If yes, it should be a Server Component so the data never touches the client. If no, keep going.

Is it purely presentational - takes props, renders markup? Leave it a Server Component. It costs nothing on the client and there is no reason to ship it.

Is it an interactive element inside an otherwise static section? Make just that element a Client Component and leave its surroundings on the server.

The default answer is always Server Component. You should be able to justify every "use client" you write. If you cannot, it probably does not belong there. This discipline is part of the broader frontend architecture we apply on every build.

The Traps Worth Knowing

A few sharp edges catch people repeatedly.

You cannot pass functions as props from a Server Component to a Client Component - they are not serialisable. Pass data, not behaviour, across the boundary.

Context providers are Client Components, so wrapping your app in a provider pulls the boundary up. Put providers as low as they can go, and prefer passing server-fetched data down as props over reaching for context.

Environment variables and secrets are safe in Server Components and exposed in Client Components. A secret read inside a "use client" file is a secret in the browser bundle. Keep anything sensitive strictly server-side. This is the same discipline we apply to API structure and security.

Third-party libraries that use hooks or browser APIs are effectively client code. Importing one into a Server Component fails. Wrap it in a thin Client Component instead of converting the whole tree.

Fetching Data Across the Boundary

The most common real-world question is not about a button. It is about data: this component needs to fetch something and also be interactive. Where does it go?

The answer is almost always to split the concern. Fetch the data in a Server Component, then pass it as props into a Client Component that handles the interaction. The Server Component owns the fetch - close to the database, no loading spinner, no exposed credentials - and the Client Component receives ready-made data and makes it interactive. You get server-side data access and client-side interactivity in the same feature, with a clean line between them.

The anti-pattern is fetching inside a Client Component with useEffect. It still works, and sometimes it is genuinely necessary - data that depends on client state, or that must refresh in response to user action. But reaching for client-side fetching by default throws away the App Router's biggest advantage. It reintroduces loading states, request waterfalls, and client-side data logic that the server could have handled before the page ever reached the browser. Use client fetching when the data genuinely depends on the client. Otherwise, fetch on the server and pass it down.

A useful mental model: data flows down from server to client as props, never back up. If you find yourself wanting to send data from a Client Component up to a Server Component, that is a signal the boundary is drawn in the wrong place, and the fix is to restructure rather than to fight it.

Why This Matters Beyond Performance

It is tempting to treat this as a performance optimisation you can defer. It is not. The server-client boundary shapes how your whole application is structured, and structure is expensive to change later.

If you start by marking everything a Client Component to move fast, you build habits and patterns that assume client-side rendering everywhere. Unwinding that later - pushing boundaries down, moving data fetching to the server, splitting components - is a refactor that touches most of the app. Getting the model right from the first component is far cheaper than retrofitting it once the codebase has grown around the wrong assumption. This is the same reason we treat architecture as an upfront decision rather than a cleanup task.

The Bottom Line

The server-versus-client decision is not a per-app setting you make once. It is a per-component judgement you make constantly, and the App Router rewards getting it right with real performance and punishes getting it wrong with bloated bundles and broken interactivity.

The model is stable once it clicks: server by default, client only where interactivity genuinely lives, boundary pushed as low as possible, and composition used to keep server content out of client bundles. Build with that instinct and the architecture stays clean as the app grows. Fight it, and you will spend your time debugging a boundary you never designed.

Building a Next.js app and want the architecture right from day one?

We build production Next.js applications with clean server and client boundaries, using AI-powered workflows to ship 40 to 60 percent faster.

Start Your Build

Tags

Next.jsReact Server ComponentsApp Routerfrontend architectureReactclient componentsperformance

V

Velox Studio

AI-Powered Development Studio

Share

Related Articles

Frontend Architecture & Best Practices

Your Next.js Project Structure Is Slowing Your Team Down

Most teams start Next.js with a flat structure that works at 10 components and breaks at 100. Here is how to organise your project so it scales with your team instead of fighting it.

7 min readRead Article
Frontend Architecture & Best Practices

React vs Next.js for Startup Projects: A Decision Framework

Next.js is React plus a set of decisions already made for you. Sometimes those decisions are a gift. Sometimes they are weight you do not need yet. Here is the honest framework for choosing.

10 min readRead Article
Frontend Architecture & Best Practices

Frontend Best Practices for 2026: The Senior Developer Checklist

The frontend basics have not changed, but the bar has. Here is the checklist senior developers actually use to keep a React codebase fast, accessible, and maintainable in 2026.

10 min readRead Article