Tracing Next.js internals to explain how React Server Components split server and client bundles from build-time transforms to runtime restoration.

We know that RSC reduces bundle size, but the actual implementation mechanism was unclear. We traced the Next.js repository code and confirmed the bundle separation process from build time to runtime.
The key question was: What does it mean for the same file to be bundled differently on the server and client?
// components/Button.tsx
'use client';
export default function Button() {
return <button onClick={() => alert('clicked')}>Click</button>;
}
// app/page.tsx
import Button from '@/components/Button';
export default function Page() {
return (
<div>
<h1>Welcome</h1>
<Button />
</div>
);
}
In this simple code, how would Button.tsx be included in the server bundle and client bundle respectively? And where does the server component Page go?
First, we traced how the 'use client' directive is processed during the build process.
Next.js analyzes files in next-swc-loader,
// packages/next/src/build/webpack/loaders/next-swc-loader.ts
const FORCE_TRANSPILE_CONDITIONS =
/next\/font|next\/dynamic|use server|use client|use cache/;
async function loaderTransform(
this: LoaderContext<SWCLoaderOptions>,
source?: string,
inputSourceMap?: any,
) {
// Check if 'use client' exists
if (shouldMaybeExclude && !FORCE_TRANSPILE_CONDITIONS.test(source)) {
return [source, inputSourceMap];
}
// Convert to SWC
return transform(source as any, programmaticOptions);
}
When this loader finds the string 'use client', it asks the Rust compiler (SWC) to translate it.
SWC converts 'use client' to a comment,
// Before conversion
'use client'
export default function Button() { ... }
// After conversion (conceptual)
/* __next_internal_client_entry_do_not_use__ default auto */
export default function Button() { ... }
This comment acts as a marker in later steps to indicate that the file is a Client Component.
Next, next-flight-loader detects this annotation and processes it differently for each bundle,
// packages/next/src/build/webpack/loaders/next-flight-loader/index.ts
export default function transformSource(
this: LoaderContext<undefined>,
source: string,
sourceMap: any,
) {
const buildInfo = getModuleBuildInfo(module);
buildInfo.rsc = getRSCModuleInformation(source, true);
// In case of Client Component
if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) {
const stringifiedResourceKey = JSON.stringify(resourceKey);
// For server bundles: replace actual code with reference
if (assumedSourceType === 'module') {
let esmSource = `
import { registerClientReference } from "react-server-dom-webpack/server";
export default registerClientReference(
function() {
throw new Error(\`Attempted to call the default export of \${stringifiedResourceKey} from the server\`);
},
${stringifiedResourceKey},
"default",
);
`;
return this.callback(null, esmSource, sourceMap);
}
}
// If it is not a Client Component, leave it as the original.
return this.callback(null, source, sourceMap);
}
'use client'export default function Button() {return (<button onClick={() => alert('clicked')}>Click me</button>)}
'use client' 지시어로 시작
The key here is that the same file is processed twice.
registerClientReference call.What does registerClientReference return?
Looking at React’s code,
// packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
export function registerClientReference(proxyImplementation, id, exportName) {
return Object.defineProperties(proxyImplementation, {
$$typeof: { value: CLIENT_REFERENCE },
$$id: { value: id },
$$async: { value: false },
// ...
});
}
The returned object is a reference object with a special $$typeof property.
{
$$typeof: Symbol.for('react.client.reference'),
$$id: '/absolute/path/to/Button.tsx#default',
$$async: false
}
This object is not an actual component, but rather a marker that says "here is a client component."
When rendering a component on the server, what happens when it encounters this reference object?
// app/page.tsx (Server Component)
export default function Page() {
return (
<div>
<h1>Welcome</h1>
<Button /> {/* Actually a reference object */}
</div>
);
}
React traverses the tree and when it finds a reference object, it serializes it as an RSC Payload.
// packages/react-server/src/ReactFlightServer.js (conceptual)
function renderElement(element) {
if (element.$$typeof === CLIENT_REFERENCE) {
// Serialize reference information
return serializeClientReference(element);
}
// Normal elements are rendered as is
}
The final generated RSC Payload,
M1: {"id":"./src/components/Button.tsx","name":"default","chunks":["client123"]}
0: ["$","div",null,{"children":[
0: ["$","h1",null,{"children":"Welcome"}],
0: ["$","@1"]
0: ]}]
{
"id": "./src/components/Button.tsx",
"name": "default",
"chunks": [
"client-123"
]
}["$","div",null,{"children":[["$","h1",null,{"children":"Welcome"}],["$","@1"]]}]
Here,
M1: Client Component definition (file path, export name, chunk ID)@1: Placeholder for "Render M1 here"When the browser receives this data,
<!DOCTYPE html>
<html>
<body>
<div>
<h1>Welcome</h1>
</div>
<script>
self.__next_f.push([
1,
'M1:{"id":"./Button.tsx","chunks":["client123"]}',
]);
self.__next_f.push([1, '0:["$","div",null,{"children":...}]']);
</script>
<script src="/_next/static/chunks/client-123.js" async></script>
</body>
</html>
processing process,
<h1>Welcome</h1> is immediately displayed on the screenself.__next_f arrayclient-123.js@1 position.the actual contents of the client bundle;
// client-chunk-123.js
export default function Button() {
return React.createElement(
'button',
{
onClick: () => alert('clicked'),
},
'Click',
);
}
The original code is included. Unlike server bundles, this is actual executable code.
To summarize the entire process,
// 1. Written by developer
'use client'
export default function Button() { ... }
// 2. SWC conversion
/* __next_internal_client_entry_do_not_use__ */
export default function Button() { ... }
// 3. next-flight-loader branch
// Server Bundle:
export default registerClientReference(...)
// Client Bundle:
export default function Button() { ... } // keep original
// 4. Server rendering
<Page>
→ <Button /> (reference object found)
→ Create RSC Payload: M1: {...}, @1
// 5. Client reception
HTML: <div><h1>Welcome</h1></div>
Payload: M1, @1
Chunk: client-123.js
// 6. Hydration
@1 location + client-123.js → Actual Button rendering
'use client'
export default function Button() {
return <button>Click</button>
}registerClientReference(
function() { throw Error() },
"/Button.tsx",
"default"
)export default function Button() {
return <button>Click</button>
}The benefits of RSC are now clear.
// Server Component
import _ from 'lodash'; // 2MB library
import { db } from './db';
export default async function Page() {
const users = await db.getUsers();
const grouped = _.groupBy(users, 'country');
return (
<div>
<UserList data={grouped} /> {/* Client Component */}
</div>
);
}
Included in server bundle only
Included in client bundle only
Send to client
The bundle is determined depending on where the same file is used. ```tsx // Use only on servers → Only in server bundles import _ from 'lodash';
// Only used on clients → Only on client bundles ('use client'); import _ from 'lodash';
// Use both → both bundles
---
Lastly, to summarize the key points:
### 1. ‘use client’ is a build time marker
It does not change runtime behavior, but is a marker that webpack uses to split bundles. The pipeline from SWC → next-flight-loader handles this.
### 2. Same file, different processing
The Client Component files are bundled twice.
- For servers: replaced by reference object
- For clients: maintain original code
### 3. Serialization is key
The core of RSC is the process of serializing a reference object on the server into an RSC Payload and restoring it to an actual component on the client.
### 4. Bundle separation is automatic
Developers only need to write `'use client'` and the build system will take care of the rest. webpack analyzes the dependency graph and creates the optimal bundle.
---
## Finish
RSC's unbundling wasn't just "magic"; it was the result of a structured build pipeline.
One line `'use client'` sets the whole process in motion: SWC conversion → webpack loader → reference object creation → RSC Payload serialization → hydration.
By understanding this mechanism, you can clearly determine “why it works this way” and “what code goes where” when using RSC.
Deeper implementation details can be found in the Next.js and React repositories.
- `packages/next/src/build/webpack/loaders/next-flight-loader/`
- `packages/react-server-dom-webpack/src/`