Server Components render to static HTML with zero client-side JavaScript — marking everything 'use client' adds 240KB+ of JS and 2.1s slower Time to Interactive
The server-client boundary is one-way: Server Components can import Client Components, but Client Components cannot import Server Components
Props passed from Server to Client Components must be serializable — functions, Date objects, and React elements are silently dropped
'use client' is not a performance setting — it's a module boundary declaration. Every child of a Client Component is automatically client-rendered
The optimal pattern: Server Components at the page/layout level for data fetching and static content, Client Components as leaf nodes for interactivity only
✦ Definition~90s read
What is Server Components vs Client Components in Next.js 16?
Server Components are a React paradigm where components render on the server and send zero JavaScript to the client. Introduced in React 18 and stable in Next.js 15+, Server Components enable pages that combine server-rendered static content with client-side interactive islands.
★
Server Components are like a printed menu — the text is already on the page when you look at it, no extra work needed.
The key insight: most of a page's components don't need interactivity — they display data, render content, and structure the layout. These components can render on the server as pure HTML, saving the client from downloading, parsing, and executing JavaScript for content that doesn't change.
The 'use client' directive marks the server-client boundary. A file with 'use client' at the top is a Client Component — it's included in the JavaScript bundle, hydrated on the client, and can use React hooks, browser APIs, and event handlers. A file without the directive is a Server Component — it renders on the server, cannot use hooks or event handlers, and contributes zero JavaScript to the client bundle.
This is not a performance optimization toggle; it's a fundamental architectural decision about where rendering happens.
The server-client boundary is one-way: Server Components can import and render Client Components, but Client Components cannot import Server Components. This restriction exists because Server Components render once on the server and their output is static, while Client Components re-render on the client.
If a Client Component imported a Server Component, the server would need to re-render on every client interaction — defeating the purpose of server rendering. The workaround is the children prop pattern: pass server-rendered content as children to a Client Component wrapper.
The performance impact of 'use client' placement is dramatic. A single 'use client' at the root layout level can turn a 85KB page bundle into a 400KB+ bundle because every child component becomes client-rendered. The optimal pattern: Server Components at the layout and page level for data fetching and static rendering, with thin Client Component wrappers at leaf nodes for interactivity.
This keeps 80-95% of the page as zero-JS Server Components while preserving full interactive capability where needed.
Plain-English First
Server Components are like a printed menu — the text is already on the page when you look at it, no extra work needed. Client Components are like a digital menu with hover effects and animations — every interactive element requires a JavaScript file to download and run. If you make the entire menu digital when most of it is just text, you're making everyone wait to download code they don't need. The trick is to use printed pages for static content and reserve the digital interactivity only for parts the user actually touches.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Next.js Server Components render on the server and send zero JavaScript to the client. This is the framework's superpower: pages that are mostly static content (product descriptions, blog posts, dashboards) can ship as raw HTML without React hydration overhead. The catch: as soon as you add 'use client' to a component, that component AND ALL ITS CHILDREN become client-rendered, requiring JavaScript download, parse, and hydration before the page is interactive.
The most common mistake teams make is marking every component with 'use client' because they need a single onClick handler somewhere deep in the tree. The cost: a 240KB JavaScript bundle that takes 2.1 seconds longer to become interactive on a 3G connection. The fix: push 'use client' as deep as possible — only the leaf components that actually need interactivity should carry the directive. Everything else stays as a Server Component, rendering to static zero-JS HTML.
In this article, you'll learn the exact server-client boundary rules, the serialization constraints that govern what data can cross the boundary, the mental model for deciding when to use 'use client', and production patterns for hybrid pages that combine server-rendered content with client-side interactivity.
The Server-Client Boundary Is One-Way — And That's the Point
Server Components can import Client Components. Client Components cannot import Server Components. This one-way boundary is the single most important rule in the Next.js component model — and the most frequently violated.
The reason for the restriction: Server Components are rendered once on the server and their output is static. Client Components are rendered on every client navigation and re-render. If a Client Component imported a Server Component, the server would need to re-render that Server Component on every client interaction — breaking the zero-JS promise and adding server roundtrips for every state change.
The workaround: pass Server Components as children or props to Client Components. The Server Component renders on the server, its output becomes part of the props, and the Client Component just places that pre-rendered content in its JSX. This preserves the server-rendered output without violating the one-way rule.
Server Components Pass Their Output, Not Their Code
Server Components render once on the server; Client Components re-render on the client
Server Component output is serialized as props/children to Client Components
Client Components importing Server Components would require server roundtrips on every re-render — hence the restriction
Production Insight
A team trying to share a data-fetching component between server and client contexts spent two days fighting the 'cannot import Server Component from Client Component' error. The solution was simple: make the data-fetching logic a shared utility function (not a component), call it in the Server Component to get data, and pass the resulting props to a Client Component renderer. The Server Component fetches, the Client Component displays — clean separation of concerns.
Rule: Server Components own data fetching and static rendering. Client Components own interactivity and state. Never mix the two concerns in a single component.
Key Takeaway
Server Components import Client Components, not the reverse.
Pass Server Component output as children/props to Client Components.
Separate data fetching (Server) from interactivity (Client) — never combine in one component.
thecodeforge.io
Nextjs Server Vs Client Components
Serialization Rules — What Can Cross the Server-Client Boundary
Props passed from Server Components to Client Components must be serializable. The framework serializes the Server Component props into JSON and sends them across the network. Any value that JSON.stringify() cannot handle is silently dropped or causes a runtime error.
Serializable types:strings, numbers, booleans, null, undefined, plain objects, plain arrays, Date (converted to ISO string automatically), BigInt (Next.js 15+), Map and Set (Next.js 15+), and React elements (Server Component output passed as children).
Non-serializable types:functions (including event handlers), class instances, Symbols, Promises, Streams, and React elements created in the client context. If you pass a function as a prop to a Client Component, the function is silently dropped — no error, no warning, just undefined on the client side.
serialization-boundary.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// ============================================// Server Component — passes serialized data to Client// ============================================// This is a Server Component (no 'use client')import { ClientCard } from'./client-card'import { db } from'@/lib/db'exportasyncfunctionProductPage({ slug }: { slug: string }) {
const product = await db.query(
'SELECT * FROM products WHERE slug = $1',
[slug]
)
// Serializable: string, number, boolean, plain objects/arrays, Datereturn (
<ClientCard
title={product.title}
price={product.price}
inStock={product.stock > 0}
tags={product.tags} // Array — serializable
createdAt={product.created_at} // Date — serialized as ISO string
onSale={product.sale_price ? true : false}
/>
)
}
// ============================================// Client Component — receives serialized props// ============================================'use client'import { useState } from'react'interfaceClientCardProps {
title: string
price: number
inStock: boolean
tags: string[]
createdAt: string
onSale: boolean
}
exportfunctionClientCard(props: ClientCardProps) {
const [expanded, setExpanded] = useState(false)
// props are JSON-deserialized — they're plain data, not live referencesreturn (
<div>
<h2>{props.title}</h2>
<p>${props.price}</p>
{props.inStock ? (
<button onClick={() => setExpanded(!expanded)}>
{expanded ? 'Show less' : 'Show more'}
</button>
) : (
<p className="text-red-500">Outof stock</p>
)}
</div>
)
}
// ============================================// WRONG — passing non-serializable props// ============================================// Server Component — DO NOT DO THISexportfunctionWrongPage() {
const handleClick = () => console.log('clicked')
// handleClick WILL BE SILENTLY DROPPED — function is not serializablereturn <ClientCard onClick={handleClick} />
}
Functions Passed From Server to Client Are Silently Dropped
If you pass a function (event handler, callback) from a Server Component to a Client Component, the function is removed during serialization with no warning. The prop will be undefined on the client. Define all event handlers inside the Client Component itself, not in the Server parent.
Production Insight
A team spent half a day debugging why a button click did nothing — the onClick handler was defined in a Server Component and passed as a prop to a Client Component button. The function was silently stripped during serialization. The button rendered but onClick was undefined. The fix: move the onClick handler definition inside the Client Component.
Rule: all event handlers and callback functions must be defined inside Client Components. Never pass functions across the server-client boundary.
Key Takeaway
Only JSON-serializable types can cross the server-client boundary.
Functions are silently dropped — define all event handlers inside Client Components.
Date, Map, Set, and BigInt are serializable in Next.js 15+.
The Real Cost of 'use client' — Bundle Size and TTI Breakdown
Every component marked 'use client' adds to the client-side JavaScript bundle. The cost is not just the component's own code — it's the entire dependency tree of that component: imports, utility functions, third-party libraries, and all child components (which become client-rendered too).
A typical Next.js page with 15 Server Components and 3 Client Components ships about 85KB of JavaScript. The same page with everything marked 'use client' ships 350-400KB. On a 3G connection (1.5 Mbps download), that extra 300KB adds 1.6 seconds to download time alone — before parsing and execution. Parse time for 300KB of JavaScript is roughly 500ms on a mid-range phone. Combined: 2.1+ seconds slower Time to Interactive.
The breakdown: component code (45%), third-party library code (30%), CSS-in-JS runtime (15%), framework overhead (10%). The largest savings come from keeping third-party-heavy components (markdown renderers, charting libraries, rich text editors) as Server Components that render on the server and ship zero JS.
bundle-size-comparison.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// ============================================// Bundle size analysis: Server vs Client// ============================================// --- WRONG PATTERN: 'use client' at layout level ---// File: app/layout.tsx'use client'// This single directive client-renders EVERYTHING// Bundle: ~350KB, TTI: 3.9s on 3G
import { Sidebar } from './sidebar' // becomes client
import { DataTable } from './data-table' // becomes client
import { ChartWidget } from './chart-widget' // becomes client
import { Footer } from './footer' // becomes client// ... 43 more components, all client-renderedexportdefaultfunctionLayout({ children }) {
return (
<div>
<Sidebar />
<DataTable />
<ChartWidget />
<main>{children}</main>
<Footer />
</div>
)
}
// --- CORRECT PATTERN: 'use client' only on interactive leaf ---// File: app/layout.tsx (Server Component — NO 'use client')// Bundle: ~85KB, TTI: 1.8s on 3G
import { Sidebar } from './sidebar' // Server Component — zero JS
import { DataTable } from './data-table' // Server Component — zero JS
import { ClientSidebar } from './client-sidebar' // Only this adds JSexportdefaultfunctionLayout({ children }) {
return (
<div>
<ClientSidebar defaultCollapsed={false}>
<Sidebar /> {/* Passedas children — still server-rendered */}
</ClientSidebar>
<main>{children}</main>
</div>
)
}
// File: client-sidebar.tsx'use client'// Only this small wrapper adds JavaScript — 2KBimport { useState } from'react'exportfunctionClientSidebar({
children,
defaultCollapsed
}: {
children: React.ReactNode
defaultCollapsed: boolean
}) {
const [collapsed, setCollapsed] = useState(defaultCollapsed)
return (
<aside data-collapsed={collapsed}>
<button onClick={() => setCollapsed(!collapsed)}>
{collapsed ? 'Expand' : 'Collapse'}
</button>
<div hidden={collapsed}>{children}</div>
</aside>
)
}
A component's full dependency tree is included in the client bundle when it's marked 'use client'
Third-party libraries (markdown, charts, editors) add 50-150KB each when client-rendered
Server-rendering those same libraries produces zero JavaScript — the HTML result is already on the page
Production Insight
We rebuilt a dashboard page after discovering the root layout had 'use client' for a sidebar toggle. The page included a charting library (recharts, 45KB), a markdown renderer (react-markdown, 30KB), and a rich text preview (70KB). All three were server-renderable but became client code because of the single layout directive. After pushing 'use client' down to a 2KB sidebar wrapper, the chart, markdown, and preview reverted to zero-JS Server Components. Bundle dropped from 420KB to 95KB.
Rule: audit 'use client' directives quarterly. Run next build --debug and check which libraries contribute to the client bundle. If a library can render on the server, make sure it does.
Key Takeaway
'use client' taxes all children and dependencies — push it as low as possible.
Third-party libraries add 50-150KB each when client-rendered — server-render them when possible.
Audit client bundle with next build --debug — look for unexpectedly large client bundles.
Server vs Client Component Trade-offsComparing performance and flexibilityServer ComponentClient ComponentBundle Size0 KB (no JS sent)240KB extra per componentTime to InteractiveFast (no hydration)2.1s slowerData AccessDirect server accessRequires API callsInteractivityNone (static)Full interactivitySerializationNo restrictionOnly JSON-serializable propsTHECODEFORGE.IO
thecodeforge.io
Nextjs Server Vs Client Components
The Children Prop Pattern — How to Pass Server Content to Client Components
The most elegant way to combine Server and Client Components without violating the one-way boundary is the children prop pattern. A Client Component wraps interactive functionality around children that are rendered by a Server parent. The Server Component renders content, passes it as children to the Client Component wrapper, and the Client Component adds interactivity without importing Server code.
This pattern works because children are serialized as React elements (server-rendered output) across the boundary. The Client Component doesn't import the Server Component — it receives pre-rendered content as the children prop. The server renders the children once, and the client places that rendered content in the interactive wrapper.
Real-world use cases: an accordion or disclosure widget whose content is server-rendered markdown, a carousel whose slides are server-fetched product cards, a filterable sidebar whose items are server-fetched categories.
children-prop-pattern.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// ============================================// Children Prop Pattern — Server + Client hybrid// ============================================// --- Server Component (page.tsx) ---import { Accordion } from'./accordion-client'import { db } from'@/lib/db'exportasyncfunctionFaqPage() {
const faqs = await db.query(
'SELECT question, answer FROM faq ORDER BY position'
)
return (
<div>
{faqs.map((faq) => (
<Accordion key={faq.question} title={faq.question}>
{/* children prop — this content is server-rendered,
then passed to AccordionClientComponent */}
<article className="prose">
{faq.answer}
<footer>
Last updated: {faq.updated_at.toLocaleDateString()}
</footer>
</article>
</Accordion>
))}
</div>
)
}
// --- Client Component (accordion-client.tsx) ---'use client'import { useState } from'react'interfaceAccordionProps {
title: string
children: React.ReactNode// Server-rendered content passed here
}
exportfunctionAccordion({ title, children }: AccordionProps) {
const [open, setOpen] = useState(false)
return (
<div className="border-b">
<button
onClick={() => setOpen(!open)}
className="w-full text-left py-3 font-medium"
aria-expanded={open}
>
{title}
<span className="float-right">{open ? '−' : '+'}</span>
</button>
{open && (
<div className="pb-3">
{children} {/* Pre-rendered server content — no hydration cost */}
</div>
)}
</div>
)
}
Use Children Props to Keep Server Components as Server Components
When a Client Component needs to display server-fetched content, wrap the content in a parent Server Component and pass it as children. The Client Component never imports the Server Component — it just renders the pre-serialized children prop. This is the most ergonomic way to build hybrid components.
Production Insight
A content site with 200+ FAQ items used an accordion pattern. The original implementation had 'use client' on the FAQ page itself because the accordion needed useState. Every FAQ item was client-rendered — 200KB of JavaScript for expandable text sections. Refactoring to the children prop pattern: the FAQ page stayed as a Server Component, each FAQ item's content was server-rendered, and only the 1KB accordion wrapper was client-rendered. Zero JavaScript for the content, 1KB for the interactivity.
Rule: use the children prop pattern for any Client Component that wraps around server-fetched content. The Client wrapper handles interactivity; the children handle content.
Key Takeaway
Children props cross the server-client boundary as pre-rendered content.
Server Component renders content, Client Component adds interactivity.
This pattern keeps 95%+ of the page as zero-JS Server Components.
Third-Party Libraries — Server-Render When Possible, Client-Render Only When Necessary
Many third-party React libraries can render on the server. react-markdown, recharts, and even some rich text renderers produce HTML on the server without requiring client-side JavaScript for the initial render. The magic is checking whether the library uses client-side hooks (useState, useEffect, event handlers) — if it doesn't, it can be a Server Component, and you can mark your wrapper component accordingly.
For libraries that MUST be client-rendered (anything with state, effects, or browser API access), import them in a dedicated Client Component file. Never import a client-only library in a Server Component — the build will fail. The pattern: create a thin wrapper Client Component that imports the library and exposes only the props you need, then import that wrapper from your Server Component.
The trade-off: wrapper components add a small amount of boilerplate but keep your component tree clean. The wrapper is a few lines of code; the alternative is marking the entire page 'use client', which adds hundreds of kilobytes of JavaScript for everything else on the page.
third-party-library-pattern.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// ============================================// Wrapping client-only libraries// ============================================// --- Server Component (page.tsx) ---// Safe to import Server-renderable libraries directlyimport { MarkdownRenderer } from'@/components/server-markdown'// Must import Client Component wrappers — not the library directly// Client-side library import is isolated in the wrapper fileimport { ChartWidget } from'@/components/chart-wrapper'exportasyncfunctionReportPage() {
const data = awaitfetchReportData()
return (
<div>
{/* Server-rendered markdown — zero JS */}
<MarkdownRenderer content={data.summary} />
{/* Client-rendered chart — only 15KB for the chart, not 400KB for the page */}
<ChartWidget data={data.series} />
</div>
)
}
// --- chart-wrapper.tsx (Client Component — only this file imports recharts) ---'use client'import { BarChart, Bar, XAxis, YAxis } from'recharts'interfaceChartWidgetProps {
data: { label: string; value: number }[]
}
exportfunctionChartWidget({ data }: ChartWidgetProps) {
return (
<BarChart width={600} height={300} data={data}>
<XAxis dataKey="label" />
<YAxis />
<Bar dataKey="value" fill="#2563eb" />
</BarChart>
)
}
// --- server-markdown.tsx (Server Component — no 'use client') ---// react-markdown can render on the server — no hooks neededimportReactMarkdownfrom'react-markdown'exportfunctionMarkdownRenderer({ content }: { content: string }) {
return (
<div className="prose">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
)
}
Check Third-Party Libraries for Server Compatibility
Before importing any React library, check: does it use useState, useEffect, or browser APIs? If not, it can be a Server Component. If yes, create a dedicated Client wrapper file that imports only that library. Never import a client-only library in a Server Component file — the build will fail.
Production Insight
A BI dashboard team used recharts for 12 chart widgets on a single page. Originally, the entire page was 'use client' because the first developer assumed charts require client rendering. Bundle: 480KB. After extracting chart widgets into 12 thin Client wrappers (each 2KB of wrapper code + 15KB chart library shared), the page bundle dropped to 180KB. The data tables, filters, and navigation became zero-JS Server Components.
Rule: for every third-party library import, ask: 'Can this render on the server?' If yes, use it in a Server Component. If not, wrap it in a dedicated Client Component file.
Key Takeaway
Libraries without hooks/browser APIs can be Server Components.
Create dedicated Client wrapper files for client-only libraries.
Never import a client-only library in a Server Component file.
Context Providers and 'use client' — The Layout Provider Trap
React Context providers require 'use client' because they use React.createContext (which is client-side state). The trap: wrapping your entire layout in a Context provider means the entire layout tree becomes client-rendered — the same problem as the root layout 'use client' mistake.
The solution: create a dedicated Client Component provider that wraps {children}. Keep your root layout.tsx as a Server Component that imports and renders the provider. Children passed to the provider remain server-rendered — only the provider itself is client code.
For data that Context provides (theme, user preferences, locale), pass the initial server-side value as a prop to the provider. The provider uses the server-provided value as the Context default, avoiding a flash of wrong content before hydration.
Providers Wrap Children — They Don't Convert Them to Client
Context providers require 'use client' but children passed to them remain server-rendered
Pass initial values from server to provider as props — avoids flash of wrong content before hydration
The provider file is the only file that needs 'use client' — all children stay as Server Components
Production Insight
A team's root layout wrapped {children} in a ThemeProvider. The theme provider used 'use client'. The team assumed children passed to the provider would be server-rendered — and they were correct. However, they also had 'use client' on the layout.tsx itself 'just in case.' Removing the redundant layout-level 'use client' dropped their client bundle from 350KB to 85KB because Navbar, Footer, and all page content reverted to Server Components.
Rule: only put 'use client' on the Context provider file. The root layout and children remain Server Components. Test by removing 'use client' from layout.tsx — if everything works, it should never have been there.
Key Takeaway
Context providers need 'use client' — wrap them in dedicated files, don't mark layout as client.
Pass initial server values as props to avoid hydration mismatch.
Children of a provider remain server-rendered — the provider is just a client shell.
Interleaving Server and Client Components — Practical Patterns
In real applications, you rarely have pure-server or pure-client pages. Most pages interleave Server and Client Components in complex trees. The key patterns:
Server Component > Client Component > Server Component (via children): the children prop pattern discussed above. The inner Server Component renders on the server, gets serialized as children, and the Client Component places it.
Server Component passes data to Client Component via serializable props: the Client Component receives data, not components. The Client Component handles rendering with its own interactive logic.
Client Component calls a Server Action: the interactive element (button, form) is in a Client Component, but the mutation logic runs on the server via a Server Action.
Server Component renders a Client Component conditionally: the server decides whether to show a Client Component based on data. The Client Component isn't imported unless the condition is met — saving bundle size for unused interactivity.
When a Client Component is conditionally rendered based on server data, it's only included in the bundle if the condition is true. Use this for paywalls, premium features, admin controls — components that only a subset of users see. Next.js code-splits at the dynamic import boundary automatically.
Production Insight
An e-commerce site had a 'Quick View' modal that was rendered for every product on the search results page — 48 modals, all client-rendered. The modal included a rich image zoom library (35KB). Refactoring: make the modal a Client Component that's only mounted when the user clicks 'Quick View.' The 35KB image zoom library is only loaded when a user opens a modal, not on page load.
Rule: use dynamic imports for heavy Client Components that aren't immediately visible. React.lazy or next/dynamic with ssr: false for components below the fold.
Key Takeaway
Use children prop for embedding Server Components inside Client wrappers.
Conditional rendering avoids loading heavy Client Components for all users.
Dynamic imports with next/dynamic for below-fold or interaction-triggered Client Components.
Testing the Server-Client Boundary — What Breaks and How to Catch It
Common boundary bugs: prop serialization silently dropping values, hydration mismatches from server/client render differences, and the 'cannot import Server Component from Client Component' build error. Each has a distinct symptom and fix.
Prop serialization bugs are the hardest to catch because they fail silently. A function passed as a prop is just undefined on the client — the component renders, but the button doesn't work. No build error, no runtime error, no console warning. The only way to catch this is code review or TypeScript typing (use types that exclude Function from prop types).
Hydration mismatches occur when the server-rendered HTML differs from the first client render. Common causes: browser-only data (localStorage, viewport size), non-deterministic rendering (Math.random(), Date.now()), and missing suppressHydrationWarning for intentionally dynamic content. Next.js logs hydration mismatches to the console in development — always check after adding new Client Components.
testing-boundary.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// ============================================// Catching Common Boundary Bugs// ============================================// --- BUG 1: Silent function drop ---// Server Component passes function to Client — silently undefinedexportfunctionServerParent() {
// WRONG: onDelete is a function — will be undefined on clientreturn <ItemList onDelete={(id) => deleteItem(id)} />
}
'use client'functionItemList({ onDelete }: { onDelete?: (id: string) => void }) {
// onDelete is undefined! Button click does nothingreturn <button onClick={() => onDelete?.('123')}>Delete</button>
}
// CORRECT: define onDelete inside the Client Component'use client'import { deleteItem } from'./actions'functionItemList() {
return <button onClick={() => deleteItem('123')}>Delete</button>
}
// --- BUG 2: Non-serializable Date ---exportfunctionServerParent() {
const date = new Date('2026-07-12') // Date object — may not serializereturn <DateDisplay date={date} />
}
'use client'functionDateDisplay({ date }: { date: any }) {
// date may be {} or ISO string depending on Next.js versionreturn <time>{date}</time>
}
// CORRECT: convert to ISO string on serverexportfunctionServerParentCorrect() {
const date = newDate('2026-07-12')
return <DateDisplay date={date.toISOString()} />
}
'use client'functionDateDisplay({ date }: { date: string }) {
return <time>{newDate(date).toLocaleDateString()}</time>
}
// --- BUG 3: Hydration mismatch from localStorage ---'use client'functionThemeIndicator() {
// WRONG: localStorage read during render — different on server vs clientconst theme = localStorage.getItem('theme') || 'light'return <span>{theme}</span>
}
'use client'functionThemeIndicatorCorrect() {
const [theme, setTheme] = useState('light')
useEffect(() => {
setTheme(localStorage.getItem('theme') || 'light')
}, [])
// First render: 'light' (server + client match)// After mount: actual theme from localStoragereturn <span suppressHydrationWarning>{theme}</span>
}
Function Props Are Silent Killers — TypeScript Can Help
A function passed from a Server Component to a Client Component is silently dropped. TypeScript won't catch it. The only defense: don't define functions in Server Components that need to be passed as props. If a Client Component needs a callback, define it inside the Client Component, not in the Server parent.
Production Insight
A team's product filter component received an onChange callback from a Server Component parent. The filter appeared but didn't trigger any action on change. The team spent two days checking the filter logic before realizing the onChange handler was a function defined in the Server Component — silently dropped during serialization. The fix: move the onChange logic into the filter Client Component using a Server Action import.
Rule: audit all props passed from Server to Client Components. If a prop is a function, you have a bug. Move the function definition into the Client Component.
Key Takeaway
Function props are silently dropped across the server-client boundary.
Hydration mismatches come from browser-only data during render — use useEffect.
TypeScript can't catch serialization bugs — code review is essential.
Server Component Composition vs Client Component Composition
Server Components and Client Components compose differently. Server Components can be async and directly await data fetching. Client Components cannot be async — they must use hooks (use, useEffect) for asynchronous operations. Server Components compose at build/request time; Client Components compose at runtime on the client.
The practical impact: a Server Component tree resolves completely before any Client Component starts rendering. This means all server data is available before the first client paint — no waterfall from client data fetching. The Server Component tree acts as a data-fetching orchestration layer, and the Client Component tree acts as an interactivity layer on top of pre-fetched data.
When composing, always fetch data at the Server Component level and pass it down. Never fetch data in a Client Component if the data is available at request time. Reserve client-side fetching for data that changes after initial load (live search results, real-time updates, user-specific data after authentication).
Server Components fetch ALL data at request time — no waterfalls
Client Components filter, sort, and search already-fetched data — no extra server calls
Only fetch new data on the client for real-time updates or user-specific data unavailable at request time
Production Insight
A product listing page initially fetched data in both the Server Component (for initial render) and a Client Component (for filter/sort changes). This caused a double-fetch: 47 products loaded via the server, then the same 47 products loaded again via client fetch on hydration. Moving to client-side filtering of the server-fetched data eliminated the duplicate API calls and cut page load time by 40%.
Rule: if the data is available at request time, fetch it in a Server Component. Client-side filtering on server-fetched data is always faster than client-side re-fetching.
Key Takeaway
Server Components fetch data at request time — Client Components filter already-fetched data.
Never double-fetch: if data is available from server, don't re-fetch on client.
Client-side filtering on server data is faster than client-side API calls.
Performance Budgets for Client Components — When 'use client' Costs Too Much
Set a performance budget for client-side JavaScript. A reasonable budget: 100-150KB per page for a content site, 200-300KB for a dashboard. If your page exceeds this, audit 'use client' usage. The most common offenders: charting libraries (40-60KB), rich text editors (100-200KB), animation libraries (30-50KB), and icon sets (20-100KB).
Track bundle size per page with next build --debug or @next/bundle-analyzer. Set up CI checks that fail if client bundle exceeds your budget. Monitor bundle size changes in every PR — a 'use client' added to the wrong component can silently add 50KB+ to every page that imports it.
The cost calculation: for every 100KB of JavaScript on a mid-range phone (Moto G4, 3G connection): 400ms download, 200ms parse, 150ms execute = 750ms added to TTI. A 300KB budget overage adds 2.25 seconds to TTI — the difference between a 'fast' page and a 'slow' page in Core Web Vitals.
Every 100KB of Client JS Adds ~750ms to TTI on Mid-Range Phones
Bundle size directly impacts Core Web Vitals. A 300KB budget overage can push your TTI past 4 seconds on 3G — failing the 'Good' threshold for Largest Contentful Paint and Total Blocking Time. Optimize 'use client' usage before optimizing image sizes or font loading.
Production Insight
A team with a 650KB client bundle reduced it to 180KB by: (1) removing 'use client' from the root layout (saved 200KB), (2) server-rendering react-markdown content (saved 30KB), (3) lazy-loading recharts below fold (saved 45KB), (4) replacing a heavy icon library with inline SVGs (saved 95KB). TTI improved from 5.2s to 2.1s. The team's Lighthouse performance score went from 48 to 92.
Rule: measure client bundle size per page, not globally. A small bundle on the marketing site doesn't excuse a 600KB bundle on the dashboard. Set per-page budgets and enforce them in CI.
Key Takeaway
Set a per-page client bundle budget: 100-150KB for content sites, 200-300KB for dashboards.
Use next build --debug or @next/bundle-analyzer to track bundle size per page.
Every 100KB of client JS adds ~750ms to TTI on mid-range devices.
Converting Legacy Client Components to Server Components — A Refactoring Playbook
Converting a Client Component to a Server Component requires checking three things: (1) does it use hooks (useState, useEffect, useContext, useReducer, useCallback, useMemo)? (2) does it use browser APIs (window, document, localStorage)? (3) does it use event handlers (onClick, onChange, onSubmit)? If yes to any, it must stay client. If no to all, it can be a Server Component.
For components that partially need interactivity, split them: extract the interactive part into a small Client Component wrapper and keep the rest as Server. Example: a ProductCard with a 'Add to Cart' button can be split into a Server ProductCard (renders product info, image, price) that imports a Client AddToCartButton.
The conversion saves 100% of the component's JavaScript from the client bundle. For a component with heavy third-party dependencies, the savings can be dramatic — a markdown-rendered article component drops from 45KB to 0KB.
When converting a Client Component to Server, don't delete it — split it. Extract the interactive element (button, toggle, dropdown) into a separate Client Component file. The original component becomes a Server Component that imports the extracted Client piece. The JavaScript for the original component and its third-party dependencies drops to zero.
Production Insight
A team converted 23 Client Components to Server Components over two weeks. The process for each: identify the interactive parts, extract them into tiny Client wrapper files, remove 'use client' from the original. Result: client bundle dropped from 520KB to 140KB. TTI improved from 4.8s to 2.2s. The team's rule: 'If a component doesn't use hooks or events directly, it should not have 'use client'.'
Rule: before adding 'use client' to any component, ask: 'Can I extract the interactive part and leave the rest server-rendered?' The answer is almost always yes.
Key Takeaway
Convert Client to Server by extracting interactive elements into separate wrapper components.
If a component doesn't use hooks/events/browser APIs directly, it can be a Server Component.
Splitting drops client JS from 45KB to 1KB for typical content-display components.
Server Actions as the Bridge — Interactivity Without Client State
Server Actions allow Client Components to perform mutations without complex client state management. A button click in a Client Component can call a Server Action that mutates data and revalidates the cache — no API route, no fetch call, no client-side state for the result.
The pattern: use Server Actions for mutations that should feel instant (upvote, bookmark, add to cart). The Client Component calls the action on click, the server mutates, and revalidates. The page refreshes with fresh data from the cache. The user perceives the action as instant even though the server roundtrip takes 100-300ms — because the button state changes immediately (optimistic update via useActionState).
For read-heavy pages with occasional mutations, this pattern keeps almost all code as Server Components. The only client code is the tiny button component that triggers the Server Action.
Server Actions = Client-Style Interactivity Without Client State
Server Actions run mutations on the server but feel instant with useActionState optimistic updates
The Client Component for triggering a Server Action can be as small as 10 lines
Most of the page stays server-rendered — only the action-triggering buttons are client code
Production Insight
A social news site had a 'bookmark' button on 50 items per page. Originally, each bookmark button was part of a Client Component that managed bookmark state in React context — 80KB of client JS for bookmark management. Rewriting to Server Actions: each bookmark button is a standalone Client Component (< 1KB) that calls a Server Action. Total client JS for bookmarking: 50KB (50 instances × 1KB). The bookmark state management moved from client context to server database. Simpler, smaller, faster.
Rule: Server Actions are the ideal pattern for small interactive elements on otherwise server-rendered pages. If your interactivity is a single button click, use a Server Action.
Key Takeaway
Server Actions let Client Components trigger mutations without complex client state.
Tiny button components (< 1KB) with Server Actions keep 95%+ of the page server-rendered.
UseActionState provides optimistic updates for instant-feeling interactions.
The Future — Partial Prerendering and the Server-First Default
Next.js 16's Partial Prerendering (PPR) takes the server-first model further. With PPR, a page can mix static shell, dynamic server content, and client interactivity in a single response. The server renders the static parts immediately (cached HTML), streams dynamic server content as it's ready, and the client hydrates only the interactive islands.
The mental model shift: default to Server Components. Add 'use client' only for interactive islands that cannot render on the server. With PPR, even Server Components that fetch dynamic data can be streamed — the page is interactive sooner because only the truly dynamic parts wait for data.
PPR doesn't change the boundary rules. Server Components still cannot import Client Components in the reverse direction. Serialization rules still apply. But PPR makes the performance cost of Client Components more visible — if a page isn't streaming fast, check if too much content is behind 'use client' boundaries.
ppr-server-first.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ============================================// PPR with Server-First Default// ============================================// --- PPR page: static shell, dynamic content, client islands ---// next.config.ts — enable PPRexportdefault {
experimental: {
ppr: true,
},
}
// app/page.tsx — PPR-enabled pageexportdefaultasyncfunctionHomePage() {
return (
<>
{/* Static shell — cached, instant */}
<header>
<StaticNav />
</header>
{/* Dynamic content — streamed when ready */}
<Suspense fallback={<DashboardSkeleton />}>
<DashboardStats />
</Suspense>
{/* Client island — hydrated on client */}
<InteractiveChart />
</>
)
}
// --- Performance comparison ---// Without PPR + 'use client' everywhere: 4.2s TTI, 400KB JS// With PPR + Server-first: 1.5s TTI (static shell), 2.1s (full), 85KB JS//// The static shell (header, skeleton) is visible in 1.5s// Dynamic stats stream in around 2.5s// Chart hydrates on client — only 15KB of chart JS
Server Components with PPR stream their content — no waiting for server data to hydrate
The fewer Client Components, the more of the page is available in the static shell
Production Insight
Early PPR adopters report 40-60% improvement in LCP on dashboard pages. The pattern: static shell (layout, nav, sidebar structure) is served from cache instantly. Dynamic widgets (stats, charts) stream as their data queries complete. Client interactivity (filters, toggles) hydrates in place. The page feels interactive within 1.5 seconds even if the full dataset takes 3 seconds to arrive.
Rule: with PPR, think of your page as three layers: (1) static shell — instant, cached, zero JS; (2) dynamic server content — streamed, zero JS; (3) client islands — interactive, hydrated, bundled. Minimize layer 3.
Key Takeaway
PPR makes server-first the default — static shell, streamed dynamic, hydrated client islands.
'use client' should only appear on interactive islands — 5-15% of components max.
PPR with server-first architecture improves LCP by 40-60% on data-heavy pages.
● Production incidentPOST-MORTEMseverity: high
240KB Extra JavaScript — 'use client' on a Layout Component
Symptom
Lighthouse performance score dropped from 94 to 61 after a refactor. Time to Interactive increased from 1.8s to 3.9s. The JavaScript bundle grew from 85KB to 325KB. The page had no interactivity on 80% of its surface area — only a sidebar collapse button needed JavaScript.
Assumption
The team assumed 'use client' was about component functionality, not about the rendering boundary. They thought adding it to the layout was harmless because the layout itself had only one interactive element. They didn't realize 'use client' marks every child component as client-rendered too.
Root cause
The root layout component (app/layout.tsx) had a single 'use client' directive at the top because the sidebar used useState for collapse state. This caused all 47 child components — data tables, charts, navigation links, footer — to be client-rendered. Every component that could have been a zero-JS Server Component was now shipped as JavaScript, parsed, and hydrated. The 240KB of extra JavaScript came from component code that would normally be stripped during SSR but now had to be bundled for the client.
Fix
1) Moved the 'use client' directive from the root layout down to a new ClientSidebar component. 2) The root layout became a Server Component that imports ClientSidebar as a child. 3) All other children (data tables, charts, nav) became Server Components again. 4) The sidebar collapse state was lifted to a Client Component wrapper that accepts collapsed={false} as a prop from the server — the server renders the initial state, the client hydrates only the interactive parts. 5) Bundle size dropped from 325KB to 85KB. TTI went from 3.9s to 1.9s.
Key lesson
'use client' applies to the component AND all its children — adding it high in the tree client-renders the entire subtree
Push interactivity to leaf components — Server Components should be the default, Client Components the exception
Root layouts should almost always be Server Components — wrap interactivity in dedicated Client Component children
A single 'use client' at the wrong level can increase bundle size by 200-300KB and degrade TTI by 2+ seconds
Production debug guideDiagnose and fix 'use client' boundary violations in production5 entries
Symptom · 01
Next.js build error: 'You're importing a component that needs useState. But the parent component is a Server Component'
→
Fix
The parent component is missing 'use client' but tries to use client-side hooks (useState, useEffect, onClick, etc.). Either add 'use client' to the parent (if it genuinely needs interactivity) or extract the interactive part into a separate Client Component and import it from the Server parent.
Symptom · 02
'You cannot import a Server Component from a Client Component' build error
→
Fix
A Client Component is trying to import a Server Component. This violates the one-way boundary. Restructure: pass the Server Component as a children prop to the Client Component, or render the Server Component in a parent Server Component and pass its rendered output as props.
Symptom · 03
Page loads but interactive elements (buttons, toggles) are slow to respond
→
Fix
The JavaScript bundle is too large because too many components are client-rendered. Run next build --debug and check the client bundle size. Look for 'use client' directives high in the component tree. Push them down to leaf components only.
Symptom · 04
Props passed to Client Component appear as undefined or empty
→
Fix
Check if the props are serializable. Client Components receive props as JSON serialized over the wire. Functions, Date objects, Map, Set, BigInt, and React elements are NOT serializable. Convert Date to ISO string, pass data instead of functions, and never pass React elements as props.
Symptom · 05
Build succeeds but runtime error: 'Text content did not match' hydration mismatch
→
Fix
The server-rendered HTML differs from the client-rendered HTML. Common causes: using browser-only APIs (localStorage, window) in a Client Component without checking if mounting has occurred, or generating dynamic content that differs between server and client renders. Use useEffect for browser-only initialization and suppressHydrationWarning where appropriate.
★ Server Component Boundary Quick Debug ReferenceFast commands and checks for diagnosing server-client boundary issues
'use client' causing large bundle size−
Immediate action
Find all 'use client' directives and check if they're too high in the tree
Commands
grep -rn "'use client'" app/ --include='*.tsx' --include='*.ts' | head -30
next build --debug | grep -E 'Route \(app\)|First Load JS'
Fix now
Move 'use client' from layout/page level to individual leaf components. Wrap interactive parts in small Client Component wrappers.
Server Component import error in Client Component+
Immediate action
Check if a Client Component is directly importing a Server Component
and ALL its children — as client-rendered. One directive at the right level can add 240KB+ to your bundle
2
The server-client boundary is one-way
Server Components can import Client Components, but not vice versa. Use the children prop pattern to embed server content in client wrappers
3
Props crossing the boundary must be JSON-serializable
functions are silently dropped, Date needs explicit conversion
4
Push 'use client' as deep as possible. Interactive leaf components only. Root layouts, pages, and data-fetching components should be Server Components by default
5
Third-party libraries without hooks/browser APIs can be Server Components
server-render them for zero JS cost
6
Server Actions bridge the gap
tiny Client Components trigger server mutations without complex client state management
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain the server-client component boundary in Next.js. Why is it one-w...
Q02SENIOR
How do you decide which components should be Server Components versus Cl...
Q03SENIOR
What are the serialization constraints when passing props from Server to...
Q04SENIOR
Describe a production incident involving 'use client' misuse and how you...
Q05SENIOR
What is the children prop pattern and when should you use it?
Q01 of 05SENIOR
Explain the server-client component boundary in Next.js. Why is it one-way, and how do you work around it?
ANSWER
The boundary is one-way: Server Components can import Client Components, but Client Components cannot import Server Components. This is because Server Components render once on the server to static HTML, while Client Components re-render on the client with full React lifecycle. If a Client Component imported a Server Component, the server would need to re-render that component on every client state change — defeating the purpose of server rendering and adding latency. The workaround is the children prop pattern: a Server Component renders content and passes it as children to a Client Component wrapper. The Client Component never imports the Server Component — it receives pre-rendered output as a prop. This preserves the one-way boundary while allowing Server-rendered content inside interactive Client wrappers.
Q02 of 05SENIOR
How do you decide which components should be Server Components versus Client Components?
ANSWER
Server Components should be the default for every component. Only add 'use client' when the component directly uses React hooks (useState, useEffect, useContext, useReducer), browser APIs (window, document, localStorage), or event handlers (onClick, onChange). If a component's children need interactivity but the component itself doesn't, extract the interactive part into a small Client Component wrapper and keep the parent as a Server Component. For third-party libraries: check if the library uses hooks or browser APIs. If it doesn't, it can be a Server Component (e.g., react-markdown). If it does, create a dedicated Client wrapper file that imports the library. Never add 'use client' to a component just because you're importing a Client Component — the boundary is per-file, not per-import-chain.
Q03 of 05SENIOR
What are the serialization constraints when passing props from Server to Client Components, and how do you handle non-serializable data?
ANSWER
Props are serialized as JSON across the server-client boundary. Only JSON-compatible types are supported: strings, numbers, booleans, null, undefined, plain objects, and plain arrays. Next.js 15+ also serializes Date (as ISO string), BigInt, Map, and Set. Functions, class instances, Symbols, Promises, and Streams are NOT serializable. Functions are silently dropped — the client receives undefined. To handle non-serializable data: convert Date objects to ISO strings before passing, replace callback functions with Server Actions (callable from Client Components via import), convert custom class instances to plain objects, and pass React elements only as the children prop (not as named props).
Q04 of 05SENIOR
Describe a production incident involving 'use client' misuse and how you fixed it.
ANSWER
A SaaS dashboard team added 'use client' to the root layout component for a sidebar collapse button. This caused all 47 child components — data tables, charts, navigation, footer — to become client-rendered. The JavaScript bundle grew from 85KB to 325KB, and Time to Interactive increased from 1.8s to 3.9s. The fix: extracted the sidebar collapse state into a separate Client Component (ClientSidebar, 2KB), removed 'use client' from the root layout, and passed sidebar content as children from the Server layout to the Client wrapper. All 47 child components reverted to zero-JS Server Components. The bundle dropped to 85KB and TTI returned to 1.9s. The lesson: 'use client' at the root level is almost never necessary. Push it down to interactive leaf components only.
Q05 of 05SENIOR
What is the children prop pattern and when should you use it?
ANSWER
The children prop pattern is a way to embed Server Component content inside Client Components without violating the one-way boundary. A Server Component renders content and passes it as the children prop to a Client Component wrapper. The Client Component adds interactive behavior (toggles, accordions, modals) around the pre-rendered server content. This works because children are serialized as React elements across the boundary. Use this pattern for: accordions with server-fetched content, modals with server-rendered details, carousels with server-fetched slides, and any interactive wrapper around primarily static content. The benefit: the content uses zero client JavaScript, and only the small interactive wrapper is included in the client bundle.
01
Explain the server-client component boundary in Next.js. Why is it one-way, and how do you work around it?
SENIOR
02
How do you decide which components should be Server Components versus Client Components?
SENIOR
03
What are the serialization constraints when passing props from Server to Client Components, and how do you handle non-serializable data?
SENIOR
04
Describe a production incident involving 'use client' misuse and how you fixed it.
SENIOR
05
What is the children prop pattern and when should you use it?
SENIOR
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
What exactly does 'use client' do in Next.js?
'use client' is a directive that marks the module boundary between server-rendered and client-rendered components. Components in files with 'use client' are rendered on the client — they're included in the JavaScript bundle, hydrated, and can use React hooks (useState, useEffect, etc.), browser APIs, and event handlers. All child components of a 'use client' component are also client-rendered, even if their files don't have the directive.
Was this helpful?
02
Can I have both Server and Client Components in the same file?
No. A file is either a Server Component (no directive) or a Client Component ('use client' at the top). You cannot mix both in the same file. Server Components and Client Components must be in separate files. The rule: create a dedicated file for each Client Component wrapper, and keep Server Components in files without 'use client'.
Was this helpful?
03
How do I pass data from a Server Component to a Client Component?
Pass serializable props. The Server Component fetches data and passes it as props to the Client Component. The data is serialized as JSON across the boundary. Supported types: strings, numbers, booleans, null, undefined, plain objects, plain arrays, Date (serialized as ISO string), BigInt, Map, and Set. Functions, class instances, and React elements (other than children) are NOT serializable.
Was this helpful?
04
What happens if I pass a non-serializable prop to a Client Component?
Functions and class instances are silently dropped — the prop will be undefined on the client with no build error or runtime warning. Other non-serializable types (Symbols, Promises) may cause a serialization error at build time or during server rendering. Always check that your props are plain data objects before passing from Server to Client.
Was this helpful?
05
Why does adding 'use client' to my layout increase the bundle size so much?
'use client' on a layout component client-renders the layout AND all its children — every page component, every navigation element, every footer. The entire component tree under that layout becomes part of the client JavaScript bundle. A single 'use client' on the root layout can turn a 85KB page bundle into a 400KB+ bundle.
Was this helpful?
06
Can I use React Context in Server Components?
No. React Context (createContext, useContext) requires client-side state and cannot be used in Server Components. However, you can wrap your Server Component tree with a Client Component provider. The provider is a thin Client Component that wraps {children}, and the children remain server-rendered. Pass initial context values from the server as props to the provider.