React Server Components vs Client Components: When to Use Each

React Server Components (RSC) changed how we think about component boundaries. The rule is simple: components run on the server by default in frameworks like Next.js 15, and only components with interactive state or browser APIs need the 'use client' directive. Knowing where to draw that line makes the difference between a fast, lightweight app and one weighed down by unnecessary JavaScript.
What Server Components Actually Do
Server Components run exclusively on the server during rendering. They can access databases, file systems, and backend services directly. The output is serialized as RSC payload — not HTML, but a compact stream that React reconciles on the client without shipping the component's JavaScript.
// This runs on the server — zero JavaScript to the client
export default async function ProductDetails({ id }: { id: string }) {
const product = await db.product.findUnique({ where: { id } })
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ServerEvaluatedPrice productId={product.id} />
</article>
)
}
No useEffect for data fetching. No loading states for initial data. The HTML arrives fully rendered. Lighthouse scores see an immediate improvement because there is less JavaScript to parse and execute.
When to Reach for 'use client'
Add the client boundary only when a component needs something the server cannot provide: event handlers, browser APIs, React hooks like useState or useEffect, or browser-only objects like window and document.
'use client'
import { useState } from 'react'
export function AddToCart({ productId }: { productId: string }) {
const [quantity, setQuantity] = useState(1)
return (
<div className="flex items-center gap-2">
<button onClick={() => setQuantity((q) => Math.max(1, q - 1))}>-</button>
<span>{quantity}</span>
<button onClick={() => setQuantity((q) => q + 1)}>+</button>
</div>
)
}
The key insight: you can keep server components in the parent wrapping client components. Pass data as props. This keeps your bundle small while still providing interactivity where it matters.
The Composition Pattern That Scales
The most effective architecture pushes 'use client' to the leaf nodes. Your page-level and layout components stay as server components. Client components consume server-rendered data through props or children.
// ✅ Good — server component wraps client component
export default async function Page() {
const products = await getProducts()
return <ProductList products={products} />
}
// ProductList is a client component but receives data as props
'use client'
export function ProductList({ products }: { products: Product[] }) {
const [filter, setFilter] = useState('')
// ...
}
This pattern lets the server do the heavy lifting while the client only handles interactive state. It is the single most impactful performance decision you can make in a Next.js application.
Common Mistakes and How to Avoid Them
The biggest mistake is sprinkling 'use client' on layouts or page wrappers — that ships your entire navigation and header JavaScript to every visitor. Another pitfall is importing server-only code (database clients, environment secrets) into a client component, which leaks those imports into the browser bundle.
// ❌ Never do this — imports server code into the browser
'use client'
import { db } from '@/lib/db' // This ends up in the client bundle
Use server-only packages or keep data-fetching logic in server components that pass results down as props. Libraries like t3-oss/server-only enforce this at build time.
Measuring the Impact
Monitor your bundle size with tools like @next/bundle-analyzer. A well-architected RSC application should see 40-60% less JavaScript on initial page load compared to an all-client approach. Track the ratio of server component bytes to client component bytes — your target is at least 70% server-sourced content.
Building an optimal component architecture requires experience with RSC boundaries and server state patterns. At SoniNow, we specialize in React performance architecture, helping teams identify components that can move server-side and trimming unnecessary client bundles to deliver sub-second load times.
Need help optimizing your React app? Talk to our team about your current architecture and let us build a performance roadmap together.
Related Insights

Building Accessible React Applications: WCAG 2.2 Compliance Guide
A guide to building WCAG 2.2 compliant React applications including semantic HTML, ARIA attributes, keyboard navigation, focus management, and automated accessibility testing.

API Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window
A guide to implementing API rate limiting including token bucket, leaky bucket, sliding window, and distributed rate limiting with Redis for production APIs.

Authentication Patterns in Modern Web Apps: JWT, Sessions, and Passkeys
A guide to modern authentication patterns comparing JWT, session-based auth, and passkeys including implementation strategies, security considerations, and user experience.