React Server Components: How Your Bundle Stays Small
RSC renders components exclusively on the server — their code never ships to the browser. Here's what that means in practice for AI-native apps.
May 4, 2026 · 4 min read
React Server Components (RSC) became the default rendering model in the Next.js App Router. After building several production AI-native apps with them, here's what actually matters in practice — and why they're a particularly good fit for the kind of products we ship at Jodei.
The core idea
A Server Component runs once on the server and sends only HTML to the client. Its JavaScript never reaches the browser — not the component code, not its imports, nothing. That matters because a typical data-fetching layer (a query client, an HTTP library, serialization helpers) can add 40–80 kB to your bundle before you write a single line of business logic. With RSC, that cost is zero by default: if a file has no "use client" directive, it stays on the server.
The practical effect is that your JavaScript bundle shrinks to the interactive parts of your UI — forms, dropdowns, real-time streaming widgets — and everything else is server-rendered HTML.
What you can do in a Server Component
- Fetch data with
async/awaitdirectly in the component body — nouseEffect, no loading spinner, no client-side waterfall. - Read environment variables and secrets without leaking them to the browser.
- Query your database or internal APIs without CORS configuration.
- Call expensive compute — markdown rendering, syntax highlighting, PDF parsing — without paying the bundle cost.
What you cannot do
- Use React state hooks (
useState,useReducer). - Attach event listeners or touch browser-only APIs like localStorage.
- Consume React context directly (though you can pass server-fetched data as props).
The "use client" boundary
Adding "use client" at the top of a file marks it — and everything it imports — as a Client Component. Think of it as a fence: server code lives outside, interactive code lives inside.
The key insight is that a Client Component can still receive Server Component output as children or props. Your interactive navigation toggle can live inside a server-rendered layout without pulling the layout into the client bundle:
// layout.tsx — Server Component, reads DB at request time
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await db.users.findFirst({ where: { id: session.userId } });
return <Shell user={user}>{children}</Shell>;
}
// Shell.tsx — "use client", handles mobile menu state
"use client";
export function Shell({ user, children }: { user: User; children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<div>
<nav>
<button onClick={() => setOpen(!open)}>Menu</button>
{open && <MobileMenu />}
</nav>
<main>{children}</main>
</div>
);
}The layout fetches from the database on the server. Shellhandles interactivity on the client. Neither leaks into the other's bundle.
Practical patterns for AI-native apps
RSC fits AI product patterns especially well. A few examples we use regularly:
- RAG pipelines: Fetch retrieved context and render it in a Server Component. Pass only the formatted string to a thin
<StreamOutput>client island that handles the LLM stream. - Metadata-heavy pages: Embed model names, token counts, and cost estimates server-side without a separate API call from the browser.
- Caching: Next.js wraps
fetchwith per-request deduplication and optional persistent caching. Repeat the same fetch in multiple components; it only executes once.
The one gotcha
Props that cross the server/client boundary must be serializable — plain JSON. You cannot pass a class instance, a function, or a raw Date object from a Server Component to a Client Component. Call .toISOString() on dates, serialize your models before passing them, and keep boundary props shallow.
Where to go from here
The Next.js App Router documentation covers caching layers, Suspense streaming, and parallel routes in depth. But the mental model to internalize first is simple: if it doesn't need to run in the browser, don't ship it there. RSC enforces that constraint by default, and your users' initial page loads get measurably faster as a result.