- Published on
Server Components: Stripping Away the Magic
- Authors

- Name
- Mehdi Akiki
Server Components: Stripping Away the Magic
React Server Components (RSC) often feel like magic. You write a component, it runs on the server, and somehow it appears in your browser, seamlessly integrated with your client-side interactivity. Frameworks like Next.js wrap this in layers of bundlers, routers, and build steps, making it hard to see what's actually happening.
Let's peel back all those layers with a concrete example and see exactly what goes over the wire.
The Component We'll Trace
Here's a simple Next.js page using Server Components:
// app/page.tsx (Server Component by default)
async function ServerPage() {
// This runs on the server
const data = await fetch("https://api.example.com/user/1").then((r) => r.json());
return (
<div>
<h1>User Profile</h1>
<p>Name: {data.name}</p>
<p>Email: {data.email}</p>
<LikeButton userId={data.id} />
</div>
);
}
// components/LikeButton.tsx (Client Component)
("use client");
import { useState } from "react";
export function LikeButton({ userId }) {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>;
}
Now let's see what actually happens when you visit this page.
Step 1: Initial HTTP Request
Your browser makes a standard GET request:
GET / HTTP/1.1
Host: example.com
Accept: text/html,application/xhtml+xml
What happens on the server:
// Pseudo-code of what Next.js does internally
async function handleRequest(url) {
// 1. Server runs your component
const data = await fetch("https://api.example.com/user/1").then((r) => r.json());
// 2. Creates the component tree
const tree = {
type: "div",
children: [
{ type: "h1", children: "User Profile" },
{ type: "p", children: `Name: ${data.name}` },
{ type: "p", children: `Email: ${data.email}` },
{ type: LikeButton, props: { userId: data.id }, isClient: true },
],
};
// 3. Converts to HTML + RSC Payload
const html = renderToHTML(tree);
const rscPayload = serializeTree(tree);
return { html, rscPayload };
}
What the browser receives:
<!doctype html>
<html>
<head>
<script src="/_next/static/chunks/main.js"></script>
<script src="/_next/static/chunks/components_LikeButton.js"></script>
</head>
<body>
<div>
<h1>User Profile</h1>
<p>Name: John Doe</p>
<p>Email: [email protected]</p>
<button>❤️ 0</button>
</div>
<!-- RSC Payload embedded in script tag -->
<script type="text/x-component">
M1:{"id":"./components/LikeButton.tsx","chunks":["components_LikeButton"],"name":"LikeButton"}
J0:["$","div",null,{"children":[
["$","h1",null,{"children":"User Profile"}],
["$","p",null,{"children":"Name: John Doe"}],
["$","p",null,{"children":"Email: [email protected]"}],
["$","$L1",null,{"userId":1}]
]}]
</script>
</body>
</html>
Key Observations:
- ✅ The browser gets real HTML immediately (good for SEO and FCP)
- ✅ Server-side data (
John Doe,[email protected]) is baked into the HTML - ✅ Client component (
LikeButton) appears in HTML but also has a placeholder$L1in the payload - ✅ The client component's JavaScript bundle is included
Step 2: Understanding the RSC Payload
The RSC Payload is React's custom serialization format. Let's decode it line by line:
M1:{"id":"./components/LikeButton.tsx","chunks":["components_LikeButton"],"name":"LikeButton"}
- M1 = Module definition #1
- Says: "There's a client component at this path, load this chunk"
J0:["$","div",null,{"children":[...]}]
- J0 = JSON tree definition #0
- "$" = Special marker for React elements
- "div" = Element type
- null = Key (none in this case)
- {...} = Props
["$","$L1",null,{"userId":1}]
- $L1 = Reference to module L1 (which points to M1)
- This is where the
LikeButtonshould be inserted
Visual breakdown:
// The payload represents this structure:
{
"$$typeof": Symbol(react.element),
type: "div",
props: {
children: [
{ type: "h1", children: "User Profile" },
{ type: "p", children: "Name: John Doe" },
{ type: "p", children: "Email: [email protected]" },
{ type: MODULE_REFERENCE_1, props: { userId: 1 } } // <- Client component placeholder
]
}
}
Step 3: Client-Side Hydration
Now the JavaScript kicks in. Here's what React does on the client:
// Simplified version of React's client runtime
function hydrateRoot(container, rscPayload) {
// 1. Parse the RSC payload
const modules = parseModules(rscPayload); // Extracts M1 definitions
const tree = parseTree(rscPayload); // Extracts J0 tree
// 2. Load client component modules
const clientComponents = {};
for (const [id, moduleInfo] of modules) {
// Load the actual JavaScript for client components
clientComponents[id] = await import(moduleInfo.chunks[0]);
}
// 3. Reconstruct the React tree
function reconstructElement(node) {
if (node[0] === '$' && node[1].startsWith('$L')) {
// This is a client component reference!
const moduleRef = node[1]; // e.g., "$L1"
const moduleId = moduleRef.replace('$L', 'M'); // "M1"
const Component = clientComponents[moduleId];
const props = node[3];
return <Component {...props} />;
}
// Regular element
const [_, type, key, props] = node;
return createElement(type, { key, ...props });
}
const reactTree = reconstructElement(tree);
// 4. Hydrate the existing DOM
hydrateExistingDOM(container, reactTree);
}
What this means:
- React parses the text payload into a tree structure
- When it finds
$L1, it knows to insert theLikeButtoncomponent - It loads
components_LikeButton.jswhich contains the actualLikeButtoncode - It creates a live React component instance with
userId={1} - It attaches event listeners to the button (the
onClick)
Before hydration:
<button>❤️ 0</button>
<!-- Static HTML, no interactivity -->
After hydration:
<button>❤️ 0</button>
<!-- Now interactive! onClick works! -->
Step 4: Client-Side Navigation (The Interesting Part!)
Let's say we add another page:
// app/profile/[id]/page.tsx
async function ProfilePage({ params }) {
const user = await fetch(`https://api.example.com/user/${params.id}`).then((r) => r.json());
return (
<div>
<h1>{user.name}'s Profile</h1>
<LikeButton userId={user.id} />
</div>
);
}
Now you click a link: <Link href="/profile/2">View Profile</Link>
Traditional navigation (full page reload):
GET /profile/2 HTTP/1.1
Accept: text/html
→ Server returns full HTML document
→ Browser unloads current page
→ Browser parses and renders new HTML
→ All JavaScript state is lost
RSC navigation (soft navigation):
// React's Link component does this internally
function handleLinkClick(href) {
// 1. Intercept the click
event.preventDefault();
// 2. Fetch just the RSC payload (not HTML!)
const response = await fetch(href, {
headers: {
'Accept': 'text/x-component', // <- Key difference!
'RSC': '1'
}
});
const newPayload = await response.text();
// Returns something like:
// M1:{"id":"./components/LikeButton.tsx",...}
// J0:["$","div",null,{"children":[
// ["$","h1",null,{"children":"Jane Smith's Profile"}],
// ["$","$L1",null,{"userId":2}]
// ]}]
// 3. Parse the new tree
const newTree = parseRSCPayload(newPayload);
// 4. React diffing magic
const currentTree = getCurrentReactTree();
const diff = computeDiff(currentTree, newTree);
// 5. Update only what changed
applyDiff(diff);
// - Updates h1 text: "User Profile" → "Jane Smith's Profile"
// - Creates new LikeButton instance with userId={2}
// - But if there was a shared layout, its state is preserved!
// 6. Update browser history
window.history.pushState({}, '', href);
}
Network traffic comparison:
Traditional navigation:
Request size: 1 KB (GET request)
Response size: 50 KB (full HTML + scripts)
Page state: Lost
RSC navigation:
Request size: 1 KB (GET request)
Response size: 2 KB (just the RSC payload)
Page state: Preserved (shared components)
Visual Summary: The Full Flow
┌─────────────────────────────────────────────────────────────┐
│ INITIAL PAGE LOAD │
└─────────────────────────────────────────────────────────────┘
Browser Network Server
│ │
│──── GET / (Accept: text/html) ──────────────────────>│
│ │
│ │ Run ServerPage()
│ │ Fetch data from API
│ │ Generate HTML + RSC payload
│ │
│<──── HTML + RSC Payload + JS bundles ───────────────│
│
│ (Renders HTML immediately)
│ (Loads main.js)
│ (Loads LikeButton.js)
│ (Parses RSC payload)
│ (Hydrates React tree)
│ (Attaches event listeners)
│
│ ✅ Page is interactive!
┌─────────────────────────────────────────────────────────────┐
│ CLIENT-SIDE NAVIGATION │
└─────────────────────────────────────────────────────────────┘
Browser Network Server
│ │
│ (User clicks link) │
│ Link component intercepts │
│ │
│──── GET /profile/2 (Accept: text/x-component) ──────>│
│ │
│ │ Run ProfilePage({id: 2})
│ │ Fetch data from API
│ │ Generate RSC payload ONLY
│ │ (No HTML this time!)
│ │
│<──── Just RSC Payload (2 KB) ───────────────────────│
│
│ Parse new payload
│ Diff with current tree
│ Update DOM (only changed parts)
│ Preserve component state
│
│ ✅ Navigation complete (no flicker!)
Behind the Curtain: What's Really in Those Files?
The Server Component Output:
// What the server actually serializes
{
type: "ServerPage",
props: {},
children: [
{ type: "div", props: {}, children: [
{ type: "h1", props: {}, children: "User Profile" },
{ type: "p", props: {}, children: "Name: John Doe" },
{
type: Symbol.for("react.module.reference"),
filepath: "./components/LikeButton.tsx",
props: { userId: 1 }
}
]}
]
}
// Serialized as:
"M1:{\"id\":\"./components/LikeButton.tsx\",\"chunks\":[\"components_LikeButton\"],\"name\":\"LikeButton\"}\nJ0:[\"$\",\"div\",null,{\"children\":[[\"$\",\"h1\",null,{\"children\":\"User Profile\"}],[\"$\",\"p\",null,{\"children\":\"Name: John Doe\"}],[\"$\",\"$L1\",null,{\"userId\":1}]]}]"
The Client Component Bundle:
// components_LikeButton.js (simplified)
import { useState } from "react";
export function LikeButton({ userId }) {
const [likes, setLikes] = useState(0);
return React.createElement(
"button",
{
onClick: () => setLikes(likes + 1),
},
"❤️ ",
likes
);
}
// Registered in the module map
__webpack_module_cache__["./components/LikeButton.tsx"] = LikeButton;
Conclusion
When you strip away the build tools and framework abstractions, Server Components are:
HTTP: Standard GET requests
- Initial load: Returns HTML (for browsers/crawlers)
- Navigation: Returns RSC payload (for React)
Custom Protocol: A text-based serialization format
- Describes the component tree
- References client component modules
- Includes serialized props
JavaScript: Client runtime that
- Parses the payload
- Loads client component bundles
- Reconstructs the React tree
- Efficiently updates the DOM
There's no magic—just a clever architecture that uses standard web primitives (HTTP, HTML, JS) with a custom serialization format to split work between server (data fetching, heavy computation) and client (interactivity, state management).
I build and scale reliable production systems. Open to full-time and freelance work with U.S.-based teams that value ownership and execution.
Got something in mind?
Book a Discovery Call