Server components vs client components guide: learn how to choose the right React rendering strategy in Next.js to boost performance and simplify state.

During a recent migration to the Next.js App Router, our team spent three days refactoring a dashboard that felt sluggish despite having a relatively small bundle size. We had defaulted to use client on nearly every file, assuming it was the "safe" way to keep our interactive components working. It wasn't. By rethinking our approach to server components vs client components, we shaved about 180ms off our Time to First Byte (TTFB) and significantly reduced our main-thread blocking time.
In Next.js, the decision between server and client components isn't just about where the code runs; it's about data fetching, bundle size, and security. By default, every component in the app directory is a Server Component. These components execute exclusively on the server, meaning they have direct access to your database or file system without exposing API keys or sensitive logic to the client.
When you add the 'use client' directive at the top of a file, you're not telling Next.js to "render this on the client." You're telling the framework that this component—and any modules it imports—must be bundled and sent to the browser. This is where the cost hits: every byte of JavaScript you send is a byte the user has to download, parse, and execute.
I’ve found that the simplest way to decide is to ask two questions: Does this component need state, side effects, or browser-only APIs? If the answer is "no," it stays a Server Component.
Here is how I classify them in production:
Server Components:
useState, useEffect, or useContext.Client Components:
useEffect for lifecycle management.window or document.Early in the migration, I tried wrapping an entire page in a Client Component because I needed a search input to trigger a state update. I passed the data down from a parent Server Component as a prop.
The problem? The parent component had to re-render every time the client-side state changed, and the serialized props were becoming massive. It was a classic anti-pattern. We eventually broke that pattern by using URL search parameters for search state. This allowed us to keep the data fetching logic in a Server Component while keeping the input interactive, effectively separating our data layer from our UI layer.
When you lean into server components vs client components correctly, the performance gains are real. You're effectively shipping zero JavaScript for the majority of your page structure.
TSX// app/posts/[id]/page.tsx(Server Component) import { db } from CE9178">'@/lib/db'; import LikeButton from CE9178">'./LikeButton'; export default async function Page({ params }) { const post = await db.post.findUnique({ where: { id: params.id } }); return ( <article> <h1>{post.title}</h1> <p>{post.body}</p> {/* Only this small button is sent to the client */} <LikeButton initialLikes={post.likes} /> </article> ); }
In this example, the LikeButton is the only piece of client-side code. The heavy lifting—fetching the post, parsing markdown, and generating the shell—happens on the server. If I had made the entire page a Client Component, the user would have downloaded the entire db library and the rendering logic, which is a massive waste of resources.
Do I have to use Server Components for everything? No. They aren't a silver bullet. Use them for data fetching and layout structure. Use Client Components for the "islands" of interactivity on your page.
Can I import a Server Component into a Client Component?
You can't import a Server Component directly into a Client Component because Server Components can't be bundled for the browser. However, you can pass a Server Component as a children prop to a Client Component. This is a common pattern for wrappers like Providers or Layouts.
How does this affect SEO? It improves it. Because Server Components render to HTML on the server, search engines get the fully rendered content immediately, rather than waiting for the client-side hydration to complete.
We’re still refining how we handle complex state across the tree. Sometimes, pulling everything into a shared Server Component leads to "prop drilling" hell, which we then have to solve with React Context—which, naturally, requires a Client Component. It’s a constant balancing act.
If I were starting over, I’d be even more aggressive about moving logic to the server earlier. Next time, I’ll likely spend more time architecting the data flow via URL params rather than reaching for global state managers, as that seems to be the cleanest way to maintain the boundary between the server and the client.
Component architecture that survives a growing team requires strict boundaries. Learn how we scaled our React/Next.js codebase to keep features moving fast.