Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin8/24/20269 min read

React Server Components Explained: What Actually Runs Where

React Server Components Explained: What Actually Runs Where

# React Server Components Explained: What Actually Runs Where

TL;DR: React Server Components (RSCs) render on the server and never ship their code or dependencies to the browser — only their output (plus references to any Client Components they contain) crosses the wire in a serialized format called the Flight payload. Client Components (marked 'use client') render on the server for the initial HTML but also ship their JavaScript to the browser so they can hydrate and handle interactivity. The split isn't about "pages vs. components," it's a per-component decision, and getting it wrong is usually why an App Router project ships more JS than expected.

The problem RSCs actually solve

Before Server Components, every component in a React tree was, by default, a client component: its code got bundled, sent to the browser, parsed, executed, and hydrated — even if all it did was render a paragraph of text from a database query. That's a lot of shipped JavaScript for zero interactivity.

React Server Components, introduced as a first-class primitive and adopted as the default rendering model in Next.js's App Router, split the component tree into two execution environments:

  • Server Components run only on the server (or at build time). They can read files, query a database, call a private API with a secret key, and do all of this without ever exposing that code, or its dependencies, to the client bundle.
  • Client Components run on the server once (for the initial HTML, via SSR) and then again in the browser, where they hydrate and pick up useState, useEffect, event handlers, and browser APIs.

In an App Router project (Next.js 16, currently on React 19.2), every file under app/ is a Server Component by default. You opt a file into being a Client Component by adding 'use client' as the first line — everything imported into that file, and everything it renders, also becomes part of the client bundle unless it's passed in as a prop from a Server Component parent.

A concrete component tree, annotated

Here's a realistic dashboard page. Some parts need a database read (server-only), one part needs a click handler and local state (client-only).

app/dashboard/page.tsx          [Server Component]
├── DashboardHeader              [Server Component]
├── RevenueChart                 [Client Component]  ('use client')
│     └── useState for hovered data point
├── InvoiceList                  [Server Component]
│     ├── InvoiceRow (x N)       [Server Component]
│     └── MarkPaidButton         [Client Component]  ('use client')
│           └── onClick handler, useState for pending state
└── Footer                       [Server Component]

The code for page.tsx:

// app/dashboard/page.tsx  — Server Component (no directive needed)
import { DashboardHeader } from './DashboardHeader';
import { RevenueChart } from './RevenueChart';       // 'use client'
import { InvoiceList } from './InvoiceList';
import { Footer } from './Footer';
import { db } from '@/lib/db';

export default async function DashboardPage() {
  // Runs only on the server. `db` and any credentials it needs
  // never reach the browser bundle.
  const invoices = await db.invoice.findMany({ where: { status: 'OPEN' } });
  const revenueSeries = await db.revenue.getMonthlySeries();

  return (
    <>
      <DashboardHeader />
      {/* Server Component passes serializable data as props into a
          Client Component — this is the only thing that crosses
          the boundary here. */}
      <RevenueChart data={revenueSeries} />
      <InvoiceList invoices={invoices} />
      <Footer />
    </>
  );
}
// app/dashboard/RevenueChart.tsx — Client Component
'use client';

import { useState } from 'react';

export function RevenueChart({ data }: { data: { month: string; total: number }[] }) {
  const [hovered, setHovered] = useState<number | null>(null);

  return (
    <div onMouseLeave={() => setHovered(null)}>
      {data.map((point, i) => (
        <div key={point.month} onMouseEnter={() => setHovered(i)}>
          {point.month}: {hovered === i ? `$${point.total.toLocaleString()}` : '●'}
        </div>
      ))}
    </div>
  );
}
// app/dashboard/InvoiceList.tsx — Server Component
import { MarkPaidButton } from './MarkPaidButton'; // 'use client'

export function InvoiceList({ invoices }: { invoices: Invoice[] }) {
  return (
    <ul>
      {invoices.map((inv) => (
        <li key={inv.id}>
          {inv.clientName} — ${inv.amount}
          <MarkPaidButton invoiceId={inv.id} />
        </li>
      ))}
    </ul>
  );
}

What actually ships to the browser

This is the part that trips people up: the boundary isn't "this whole page is server-rendered, this other page is client-rendered." It's per-subtree, and the server sends a specific wire format — not plain HTML, and not plain JSON — called the Flight protocol (also referred to as the RSC payload).

For the dashboard above, the initial response is a streamed, line-delimited payload where each line is a self-contained chunk: server-rendered output is inlined as serialized React elements, and anywhere a Client Component appears, the payload instead contains a reference to that component's client bundle plus its serializable props — not the component's source and not its server-side logic.

Roughly (simplified for readability — real Flight output uses numbered chunk references, not this literal shape):

0:{"invoices":[{"id":"inv_1","clientName":"Acme","amount":500}, ...]}
1:I{"chunk":"app/dashboard/RevenueChart","props":{"data":[...]}}
2:I{"chunk":"app/dashboard/MarkPaidButton","props":{"invoiceId":"inv_1"}}

What that means in practice:

  • db.invoice.findMany(...) and the Prisma client it depends on never appear in any browser-shipped file — not even in the chunk manifest.
  • RevenueChart and MarkPaidButton's component code is shipped as separate JS chunks (standard code-split bundles) so the browser can hydrate them; only their props travel through the Flight payload.
  • InvoiceList, DashboardHeader, and Footer never ship any JavaScript. Their rendered output exists only as part of the server-produced tree.

This is also why props passed from a Server Component into a Client Component must be serializable — plain objects, arrays, strings, numbers, dates, and a few React-specific types (like Promises for streaming or Server Actions themselves). You cannot pass a live database client, a class instance with methods, or a closure over server-only state as a prop; React has nothing to serialize it into on the wire.

RSC vs. Client Components at a glance

Server ComponentClient Component
Directivenone (default in app/)'use client'
Runs on serverYesYes (SSR pass) + browser (hydration)
Ships JS to browserNoYes
Can use useState/useEffectNoYes
Can access DB/secrets directlyYesNo
Can be asyncYesNo (use hooks like use() instead)
Re-renders on client interactionNo — needs a server round-trip or a client parentYes

Common mistakes with the boundary

  • Marking a whole layout `'use client'` because one button inside it needs onClick. This pulls everything under that file into the client bundle. Push 'use client' down to the smallest leaf component that actually needs interactivity, and pass server-fetched data into it as props.
  • Importing a server-only module into a file that gets used by a Client Component. Bundlers will either error or, worse, silently include server code in the client bundle. Keep data-access code in files that are never imported from 'use client' files.
  • Assuming Server Components re-render on the client. They don't — updating a Server Component's output requires a server round-trip (navigation, a Server Action, or revalidatePath/revalidateTag). If you need something to update instantly from local interaction, it has to live in a Client Component.

A security note worth knowing

Because the Flight protocol has its own deserialization logic for reconstructing the component tree and resolving references on both ends, it's also an attack surface, not just a rendering detail. In December 2025, React disclosed CVE-2025-55182 ("React2Shell"), a critical (CVSS 10.0) unauthenticated remote-code-execution vulnerability in how RSC Server Actions/Flight endpoints deserialized client-submitted payloads, which was exploited in the wild shortly after disclosure. It's a good reminder that the RSC boundary is a real network boundary — treat any RSC/Server Action endpoint like any other API surface that parses untrusted input, and keep the framework and React versions patched.

If you're weighing whether your team should restructure an existing app around Server Components — or you're seeing unexpectedly large client bundles and can't pin down why — that kind of audit is the kind of work we do in web development engagements; it's usually a few hours of tracing the import graph, not a rewrite.

FAQ

Does using Server Components mean my app has no client-side JavaScript at all?

No. Any component using state, effects, or browser events still needs 'use client' and still ships JS. RSCs reduce the amount of JS shipped by keeping non-interactive parts of the tree server-only — they don't eliminate the client bundle.

Can a Client Component import a Server Component?

Not directly by rendering it inline — a file marked 'use client' can't import and render a Server Component the way a Server Component can render a Client Component, because everything imported into a client file gets bundled for the browser. The standard pattern is to pass the Server Component as children (or another prop) from a Server Component parent, so composition happens above the client boundary.

Do React Server Components require Next.js?

No, RSCs are a React feature, but as of 2026 Next.js's App Router remains the most mature production implementation. Other frameworks and bundlers (including Parcel and Webpack-based setups) have added RSC support, but the tooling and conventions are far more established in Next.js.

Is `'use server'` the same as a Server Component?

No — 'use server' marks a Server Action (a function callable from the client that runs on the server, typically for mutations like form submissions), not a component. A file or function can be 'use server' without being a Server Component, and Server Actions are themselves invoked via the same Flight protocol.

Sources