Server/client boundary, RSC payload streaming, and zero-bundle server rendering.
React Server Components (RSC) are components that run exclusively on the server β they never ship to the browser. They can directly access databases, file systems, and server-only secrets with zero client bundle cost. RSC output is not HTML but a special wire format (RSC Payload) that React on the client uses to reconstruct the component tree without re-running the server code.
When a page loads, the server runs your Server Components, executing async database queries, file reads, or API calls directly. The output is not HTML β it's a serialized React element tree (the RSC Payload) in a custom streaming format.
When the server encounters a Client Component (marked with 'use client'), it does not render it. Instead, it inserts a CLIENT_REFERENCE placeholder in the payload with the component's module URL and props. The client fills these holes by running the Client Component in the browser.
The server streams the RSC payload as it completes each async operation. The client's React runtime receives chunks and progressively builds the component tree β Server Component output first, then Client Component shells, then hydration.
React on the client takes the streamed RSC payload and uses it to reconstruct the full component tree. Only Client Components are hydrated (event listeners attached). Server Component output is already final β no re-execution on the client.
Functions marked with 'use server' become Server Actions β RPC-style functions that the client can call like regular async functions. Under the hood, React sends a POST request to the server with the encoded arguments and streams back the new RSC payload.
File-level directive that marks a module and all its imports as Client Components. Creates a client boundary β all exports can use hooks, event handlers, and browser APIs.
Marks a function as a Server Action. Can be called from Client Components via a network request. The function runs on the server with full server-side access.
A compact streaming JSON-like format describing the server-rendered component tree. Includes CLIENT_REFERENCE holes where client components should go. Not HTML.
Server Components never appear in the client JS bundle. A Server Component that imports a 500KB markdown parser adds 0 bytes to the client.
Components without 'use client' or 'use server' can be used in both contexts. They run on the server when imported by a Server Component, and on the client when imported by a Client Component.
1// ServerComponent.tsx β runs ONLY on server2// No bundle cost, can use async/await, access DB directly3async function ProductPage({ id }) {4 const product = await db.products.findById(id); // direct DB access!56 return (7 <div>8 <h1>{product.name}</h1>9 {/* Pass server data to a client component */}10 <AddToCartButton productId={product.id} price={product.price} />11 </div>12 );13}1415// AddToCartButton.tsx β runs on client (needs onClick)16'use client';17function AddToCartButton({ productId, price }) {18 const [added, setAdded] = useState(false);1920 return (21 <button onClick={() => {22 addToCart(productId);23 setAdded(true);24 }}>25 {added ? 'Added!' : `Add for $${price}`}26 </button>27 );28}
Server Components fundamentally change the performance equation. Previously, any component that fetched data had to ship the fetching library, data transformation logic, and rendering code to the client. With RSC, all of that stays on the server. The client only receives the final rendered output plus the interactive islands (Client Components). This leads to dramatically smaller bundles and faster Time-to-Interactive.
A product detail page needs to show: product info (from DB), reviews (from API), and an interactive 'Add to Cart' button. Traditional approach: fetch all data client-side with useEffect β loading spinners β hydration overhead.
Client-side fetching means: ship the fetch logic in the bundle β render loading state β make API calls from the browser β parse responses β re-render with data. The user sees a spinner, the bundle includes API client code, and there's a waterfall: page loads β JS executes β fetch starts β data arrives.
Make ProductPage and Reviews as Server Components (the default in Next.js App Router). They fetch data on the server with direct DB/API access and send pre-rendered HTML. Only AddToCartButton is a Client Component (marked with 'use client') because it needs onClick. Zero API client code in the bundle.
Takeaway: Server Components eliminate the request waterfall by moving data fetching to the server. Only interactive components (buttons, forms, animations) need to be Client Components. This pattern typically reduces bundle size by 30-50% and eliminates loading spinners for initial data.
You're building a blog post page. It has a header (static), article body (static with syntax-highlighted code), a comment section (interactive β like/reply buttons), and a share widget (interactive β copy link, social share).
New developers often mark the entire page as 'use client' because ONE component needs interactivity. This sends the entire page's JavaScript to the browser β syntax highlighting library, markdown parser, etc. β even though 80% of the page is static content.
Keep the page as a Server Component (default). Only wrap Comment and ShareWidget with 'use client'. The header, article body, and syntax highlighting render on the server and send zero JavaScript. The client only receives the small interactive components. Think of the boundary as: 'What needs browser APIs (state, effects, event handlers)?'
Takeaway: Push the 'use client' boundary as far DOWN the tree as possible. The rule: a component should be a Client Component only if it directly uses useState, useEffect, onClick, or browser-only APIs. Everything else should stay as a Server Component for zero bundle cost.