Understanding React Server Components (RSC) vs. Client Components
The introduction of React Server Components (RSCs) in the React ecosystem marks one of the most significant paradigm shifts in modern web development. For years, React applications operated primarily on the client side, downloading massive JavaScript bundles to the browser, hydrating components, and managing state locally.
While this client-centric model offered rich interactivity, it often led to performance bottlenecks, bloated JavaScript payloads, and slower page loads—particularly on mobile devices with limited processing power. React Server Components and Client Components work together to solve these challenges, offering a hybrid architecture that separates rendering responsibilities between the server and the browser.
This comprehensive guide explores the architectural differences between React Server Components and Client Components, examines how they impact bundle size and performance, and provides clear guidelines on when to use each model in your Next.js applications.
From Traditional Client-Side Rendering to Hybrid Architectures
To understand why React Server Components were introduced, it helps to review how React applications have historically been built. In a traditional Single Page Application (SPA), the server sends a minimal HTML shell accompanied by a large JavaScript bundle. The browser downloads, parses, and executes this JavaScript to render the user interface, fetch data via client-side APIs, and attach event listeners during a process known as hydration.
While Server-Side Rendering (SSR) improved initial page loads by generating HTML on the server before sending it to the client, every component still had to be shipped to the browser and fully hydrated. This meant that even components containing static markdown or heavy utility libraries contributed to the JavaScript bundle sent to users.
React Server Components fundamentally change this equation. By executing entirely on the server, RSCs can access databases, internal microservices, and the filesystem directly, rendering HTML streams that are sent directly to the client without shipping a single byte of their component code or dependencies to the browser bundle.
Deep Dive into React Server Components (RSCs)
React Server Components are a new type of component designed to render exclusively on the server before any client-side code execution takes place. In modern frameworks like Next.js App Router, components inside the app directory are treated as Server Components by default.
Because RSCs execute on the server, they possess unique capabilities that Client Components cannot replicate:
• Direct Backend Access: Server Components can query databases, read local files, and securely connect to internal microservices without exposing sensitive API keys or database credentials to the client.
• Zero Bundle Size Impact: Dependencies utilized exclusively inside Server Components (such as markdown parsers, heavy utility libraries, or database drivers) remain on the server. They are completely excluded from the JavaScript bundle downloaded by the user's browser.
• Automatic Code Splitting: Rather than bundling the entire application upfront, the React runtime on the server streams rendered chunks to the client progressively, improving Time to First Byte (TTFB) and First Contentful Paint (FCP).
import db from '@/lib/db';
import ProductList from '@/components/ProductList';
// This is a React Server Component by default
export default async function CatalogPage() {
// Fetch data directly on the server without API routes
const products = await db.product.findMany({
take: 20,
orderBy: { createdAt: 'desc' },
});
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Product Catalog</h1>
<ProductList products={products} />
</main>
);
}
Despite their power, Server Components have strict limitations. Because they execute entirely on the server, they cannot use React hooks like useState, useEffect, or useReducer, nor can they attach browser event listeners such as onClick or onChange.
Understanding Client Components and Interactivity
Client Components are traditional React components that render on both the server (during the initial SSR pass) and in the browser (during hydration and subsequent updates). To designate a component as a Client Component, you must place the 'use client' directive at the very top of the file.
Client Components are essential for building interactive user interfaces that respond to user input, manage local state, or interact with browser-specific APIs.
'use client';
import { useState } from 'react';
export default function CounterButton() {
const [count, setCount] = useState(0);
return (
<button
onClick={() => setCount(prev => prev + 1)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
Clicked {count} times
</button>
);
}
When you use a Client Component, that component and all of its imported dependencies are included in the JavaScript bundle sent to the client. Therefore, developers should carefully push Client Components as far down the component tree as possible to minimize unnecessary client-side JavaScript execution.
Server Components vs. Client Components Comparison Matrix
Choosing between Server Components and Client Components requires understanding their distinct execution environments and capabilities. The table below summarizes their key differences:
• Execution Environment: RSCs execute exclusively on the server, whereas Client Components execute on both the server (SSR) and client (hydration/browser).
• Access to Database & File System: RSCs have direct access; Client Components do not.
• State and Lifecycle Hooks: RSCs do not support hooks like useState or useEffect; Client Components fully support all React hooks.
• Browser APIs (localStorage, window): RSCs cannot access browser APIs; Client Components have full access.
• JavaScript Bundle Size: RSCs contribute zero bytes to the client bundle; Client Components contribute their code and dependency tree to the client bundle.
Best Practices for Combining Server and Client Components
Building high-performance applications with the Next.js App Router relies heavily on proper component composition. Because Server Components cannot import Client Components without caveats, and Client Components cannot directly import Server Components, developers follow specific composition patterns.
The most effective pattern is passing Server Components as children or props into Client Components. This allows a client-side wrapper (such as a modal, accordion, or tab container) to manage interactive state while rendering static or data-heavy server-rendered content inside it.
'use client';
import { useState } from 'react';
export default function ModalWrapper({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Details</button>
{isOpen && (
<div className="modal-overlay">
<div className="modal-content">
{/* Server-rendered children passed seamlessly into client wrapper */}
{children}
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
</div>
)}
</div>
);
}
Summary
React Server Components and Client Components represent a transformative leap forward in web architecture. By keeping data-fetching and heavy computations on the server while reserving client components strictly for interactivity, developers can achieve unprecedented levels of performance, smaller bundle sizes, and superior user experiences.
Adopt Server Components as your default building block, introducing Client Components only when browser interactivity, state management, or event listeners are strictly necessary.