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?
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?
1. Identity of ‘use client’
First, we traced how the 'use client' directive is processed during the build process.
When this loader finds the string 'use client', it asks the Rust compiler (SWC) to translate it.
Conversion of SWC
SWC converts 'use client' to a comment,
typescript
// Before conversion'use client'exportdefaultfunctionButton() { ... }
// After conversion (conceptual)/* __next_internal_client_entry_do_not_use__ default auto */exportdefaultfunctionButton() { ... }
This comment acts as a marker in later steps to indicate that the file is a Client Component.
Handling of next-flight-loader
Next, next-flight-loader detects this annotation and processes it differently for each bundle,
typescript
// packages/next/src/build/webpack/loaders/next-flight-loader/index.tsexportdefaultfunctiontransformSource(this: LoaderContext<undefined>,
source: string,
sourceMap: any,
) {
const buildInfo = getModuleBuildInfo(module);
buildInfo.rsc = getRSCModuleInformation(source, true);
// In case of Client Componentif (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) {
const stringifiedResourceKey = JSON.stringify(resourceKey);
// For server bundles: replace actual code with referenceif (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",
);
`;
returnthis.callback(null, esmSource, sourceMap);
}
}
// If it is not a Client Component, leave it as the original.returnthis.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.
Server bundle: Replaced with registerClientReference call.
React traverses the tree and when it finds a reference object, it serializes it as an RSC Payload.
typescript
// packages/react-server/src/ReactFlightServer.js (conceptual)functionrenderElement(element) {
if (element.$$typeof === CLIENT_REFERENCE) {
// Serialize reference informationreturnserializeClientReference(element);
}
// Normal elements are rendered as is
}
Server Component execution Result (HTML + RSC Payload)
Client Component Reference Information
Server component code is not transmitted.
클라이언트 번들 크기 비교
React
100KB
Page 컴포넌트
50KB
lodash
200KB
DB 코드
30KB
UserList 컴포넌트
40KB
총 번들 크기
420KB
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
code
---
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/`