Published on

Server Components: Stripping Away the Magic

Authors
  • Mehdi Akiki avatar
    Name
    Mehdi Akiki
    Twitter

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 $L1 in 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 LikeButton should 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:

  1. React parses the text payload into a tree structure
  2. When it finds $L1, it knows to insert the LikeButton component
  3. It loads components_LikeButton.js which contains the actual LikeButton code
  4. It creates a live React component instance with userId={1}
  5. 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:

  1. HTTP: Standard GET requests

    • Initial load: Returns HTML (for browsers/crawlers)
    • Navigation: Returns RSC payload (for React)
  2. Custom Protocol: A text-based serialization format

    • Describes the component tree
    • References client component modules
    • Includes serialized props
  3. 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