How server components (rsc) work in Next.js
React Server Components (RSC) are components that run on the server only, shipping zero JavaScript to the browser for the server parts of your UI.
Theory
TL;DR
- Server Components execute once per request on the server. Their code never enters the client bundle.
- Think of it like a kitchen that preps and plates your data, then sends the finished dish through a dumbwaiter. The kitchen staff and recipes never come to your table.
- The output travels to the browser as an RSC payload (a binary stream), not raw JavaScript.
- Main decision rule: fetching data, accessing secrets, or using heavy libraries? Server Component. User interaction, state, browser APIs? Client Component with
'use client'. - In Next.js App Router (13+), all components are Server Components by default.
Quick example
// app/page.tsx - Server Component (no 'use client' = server by default)
import { db } from '@/lib/db'
export default async function ProblemsPage() {
// Runs on the server. The db import never reaches the browser.
const problems = await db.problem.findMany()
return <ul>{problems.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}// app/components/FilterButton.tsx - Client Component
'use client'
import { useState } from 'react'
export function FilterButton({ onFilter }) {
const [active, setActive] = useState(false)
return (
<button onClick={() => { setActive(!active); onFilter(active) }}>
Filter
</button>
)
}The Server Component fetches data and renders HTML. The Client Component handles the click. No overlap needed.
Key difference from SSR
SSR (Server-Side Rendering) renders the full React tree on the server once and ships the entire JavaScript bundle for hydration. RSC is different: server parts of the tree live permanently on the server, send no code to the client, and re-render server-side on navigation. Your 200KB charting library stays on the server. The browser only gets the output.
Decision rules
- Fetch data from a DB or file system - Server Component. Direct
await db.query...with no API layer. - Access env variables or secrets - Server Component.
process.env.DB_URLnever leaks. - Heavy dependencies (image processing, markdown parser) - Server Component. Keeps the client bundle small.
- State, hooks, or event handlers - Client Component. Add
'use client'at the top of the file. - Browser APIs (localStorage, geolocation) - Client Component only.
- Interactive forms, real-time UI - Client Component.
Comparison table
| Aspect | Server Components (default) | Client Components ('use client') |
|---|---|---|
| Execution | Node.js, once per request | Browser, after JS bundle loads |
| JS bundle impact | Zero - only RSC payload | Full component code + dependencies |
| Data access | Direct DB, files, env vars | Via props from server or fetch to API |
| Hooks / event handlers | Not available | Full support |
| Re-renders on client | Never | On state or prop changes |
| Use case | Data-heavy UI, dashboards, lists | Forms, modals, interactive islands |
How RSC works under the hood
Next.js runs Server Components in Node.js at request time. The output is serialized into an RSC payload: a binary stream with instructions for the browser's React runtime. The payload includes rendered HTML for server parts and placeholder "holes" where Client Components will be hydrated. The browser receives both the HTML and the RSC payload, inserts the static content immediately, then hydrates only the Client Component islands.
On navigation to a new route, Next.js fetches the RSC payload for that route. Server Components re-render on the server. Client Components keep their state. The browser never downloads the server component's code.
One thing I've noticed in practice: teams moving from Pages Router tend to add 'use client' everywhere out of habit. The result is a client bundle identical to what they had before. The default is server, and you opt into the client only when you actually need it.
Common mistakes
1. Using useState in a Server Component
// Wrong - build error in Next.js 13+
export default function Page() {
const [count, setCount] = useState(0) // Hooks don't exist on the server
return <div>{count}</div>
}Fix: move state to a child Client Component and pass initial data via props.
2. Fetching sensitive data in a Client Component
// Wrong - your API key is visible in the browser bundle
'use client'
export function Users() {
useEffect(() => {
fetch(`/api/users?key=${process.env.NEXT_PUBLIC_SECRET}`) // exposed
}, [])
}Fetch in a Server Component parent and pass the result as props. Keep secrets server-side.
3. Adding 'use client' to everything
Marking a component 'use client' pulls all its imports into the client bundle. A 200KB charting library becomes 200KB of client JavaScript. Default to Server, and push 'use client' down to the smallest interactive leaf.
4. Trying to import a Server Component inside a Client Component
// Wrong - client files cannot import server-only modules
'use client'
import { ServerCard } from './server-card' // breaks the server-only boundaryPass server-rendered content via children or props instead. The Server Component renders first, then passes its output down.
Real-world usage
- Vercel dashboard: Server Components fetch data tables from Prisma, Client Components handle filter inputs.
- T3 Stack (create-t3-app): server pages fetch from Clerk or tRPC, client components handle optimistic updates.
- Shadcn/UI + Next.js 14: server renders
<Table>with DB data, client handles<Dialog>open/close state. - Payment dashboards (Stripe, etc.): transaction lists are server-only, keeping the Stripe SDK out of the browser entirely.
Follow-up questions
Q: What is the RSC payload exactly?
A: A binary stream that Next.js sends alongside the HTML. It contains the rendered output of Server Components and "hole" markers for Client boundaries. The browser's React runtime reads it to patch the DOM without re-downloading server code.
Q: What is the difference between RSC and SSR?
A: SSR sends HTML plus a full JavaScript bundle so React can hydrate the whole tree on the client. RSC streams a partial payload with zero JS for server parts. Server Components never hydrate. Only Client Components get hydration.
Q: Can Server Components use React context?
A: No. Context requires a client runtime. Share data between Server Components via props, or fetch the same data in each component (Next.js deduplicates identical fetch calls automatically).
Q: How does caching work with Server Components?
A: Next.js caches fetch calls by default (revalidate: 3600). Use cache: 'force-cache' for static data or unstable_cache for non-fetch data sources like direct database queries.
Q: (Senior) A nested Suspense tree has a slow inner boundary and a fast outer one. Walk through how RSC handles this.
A: Next.js streams the RSC payload in chunks. The fast outer boundary resolves first and streams to the browser, which renders the fallback for the slow inner boundary. When the slow boundary resolves on the server, Next.js streams the replacement chunk. The browser's React runtime swaps the fallback without a full page re-render. No client JavaScript is involved in the server parts.
Examples
Basic: server fetches, client filters
// app/problems/page.tsx (Server Component)
import { db } from '@/lib/db'
import { ProblemList } from './problem-list'
export default async function ProblemsPage() {
const problems = await db.problem.findMany({
orderBy: { difficulty: 'asc' }
})
// Pass serialized data to the client component
return <ProblemList problems={problems} />
}// app/problems/problem-list.tsx (Client Component)
'use client'
import { useState } from 'react'
export function ProblemList({ problems }) {
const [filter, setFilter] = useState('all')
const filtered = filter === 'all'
? problems
: problems.filter(p => p.difficulty === Number(filter))
return (
<div>
<select onChange={e => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="1">Easy</option>
<option value="2">Medium</option>
<option value="3">Hard</option>
</select>
<ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
</div>
)
}The server fetches all problems once. Filtering runs in the browser on the already-received data. No extra API call on filter change.
Intermediate: server component as children
Client Components cannot import Server Components. But they can receive them as children. This lets you build interactive wrappers that still benefit from server rendering inside.
// components/accordion.tsx (Client Component - handles open/close)
'use client'
import { useState } from 'react'
export function Accordion({ title, children }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(!open)}>{title}</button>
{open && children}
</div>
)
}// app/faq/page.tsx (Server Component - fetches data, uses client wrapper)
import { Accordion } from '@/components/accordion'
import { db } from '@/lib/db'
export default async function FAQ() {
const items = await db.faq.findMany()
return (
<div>
{items.map(item => (
<Accordion key={item.id} title={item.question}>
<p>{item.answer}</p> {/* Server-rendered content */}
</Accordion>
))}
</div>
)
}The <p>{item.answer}</p> is rendered by the Server Component. The Accordion handles toggle state on the client. FAQ data never touches the client bundle.
Advanced: streaming with Suspense
When one part of a page is slow (a DB query that takes 2 seconds), you can stream the fast parts immediately and deliver the slow part when it's ready.
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { TeamStats } from './team-stats' // slow - complex DB aggregation
import { QuickLinks } from './quick-links' // fast - static or cached
export default function DashboardPage() {
return (
<div>
<Suspense fallback={<p>Loading stats...</p>}>
<TeamStats /> {/* Streams when ready, after slow query resolves */}
</Suspense>
<QuickLinks /> {/* Streams immediately */}
</div>
)
}// app/dashboard/team-stats.tsx (Server Component)
import { db } from '@/lib/db'
export async function TeamStats() {
const stats = await db.stats.aggregate({ _sum: { score: true } })
return <div>Team score: {stats._sum.score}</div>
}QuickLinks appears right away. TeamStats shows "Loading stats..." until the query finishes, then the server streams the real content. No JavaScript runs for either component in the browser.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.