You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
react-dom/server (react node -> html string/stream) (aka. SSR)
The purpose of react-dom/client and react-dom/server didn't change and what's added is react tree <-> rsc stream encoding process, which allows:
"Server component" nodes to get pre-evaluated into "static html" virtual node on the server,
"Client component" nodes to get replaced as "client reference" and evaluation is delayed until traditional CSR/SSR
What this achieves is mostly similar to client island architecture, but the whole convention of uniformly writing Server/Client component tree (also with server action) is considered to have a strong DX edge.
RSC intro for bundler
For "server/client component + server action" convention, bundlers need to be able to process js modules in "separate" module graphs. A following is the example of how react libraries and user code are includes into module graphs.
# react lib
react/react.react-server.js ("react-server" condition exports of "react")
react-server-dom-xxx/server.node.js
# user code
server-component.jsx (normal jsx transpilation)
client-component.jsx (transformed into client references)
server-action.jsx (normal jsx transpilation)
ssr module graph
# react lib
react/index.js
react-server-dom-xxx/client.node.js
react-dom/server.node.js
# user code
client-component.jsx (normal jsx transpilation)
server-action.jsx (transformed into server references)
browser module graph
notably, "user code" should be processed almost same as ssr module graph
# react lib
react-server-dom-xxx/client.browser.js
react/index.js
react-dom/client.browser.js
# user code
client-component.jsx (normal jsx transpilation)
server-action.jsx (transformed into server references)
Note on webpack layers
Webpack layers are not technically "separate" module graphs. Rsc and ssr module graphs above are normally included in a single server build with two layers instead.
Each js module has an layer as an additional tag for module id. For example, react module can be included in two ways (one with react-server condition and another without it) in a single server build. Also client-component.jsx and server-action.jsx are transformed differently in each layer.
(ssr)./src/entry-ssr.jsx (this entry imports "(rsc)./src/entry-server.tsx")
(rsc)./src/entry-server.jsx
(rsc)./node_modules/react/react.react-server.js
(rsc)./node_modules/react-server-dom-xxx/server.node.js
(rsc)./server-component.jsx
(rsc)./client-component.jsx (transformed into client references)
(rsc)./server-action.jsx
(ssr)./node_modules/react/index.js
(ssr)./node_modules/react-server-dom-xxx/client.node.js
(ssr)./node_modules/react-dom/server.node.js
(ssr)./client-component.jsx
(ssr)./server-action.jsx (transformed into server references)
Note on client/server reference discovery
Normally bundler starts by processing server page entries (like ./server-component.jsx above) in rsc module graph and it discovers "use client" or "use server" directives as it traverses module imports.
Notably, when rsc module graph finds "use client" directive, it needs to stop processing the actual content of the module and move it to ssr/browser module graphs. Rsc module graph only keeps track of "references" to an original modules by transforming like this:
// client-component.jsx transformed in rsc module graphimport{registerClientReference}from"react-server-dom-vite/server"exportconstCounter=registerClientReference("/hashed-id-for/client-component.jsx","Counter",)// Also, at the end of the build pipeline, it needs to generate a mapping of reference id to an chunk// where the actual modules lives in ssr/browser build, which would look like:constclientManifestBrowser={"/hashed-id-for/client-component.jsx": "/browser-build-chunk/client-component-xxx.jsx"}constclientManifestSsr={"/hashed-id-for/client-component.jsx": "/ssr-build-chunk/client-component-xxx.jsx"}
"use server" directive works in a similar way but in a reverse direction from ssr/browser module graph to rsc module graph. This can lead to arbitrary nesting of "moving modules" as described in vercel/next.js#59615.
How "moving modules" is achieved in each bundler is as follows.
For build, it coordinates three environment builds with one extra build to scan only to discover client/server references. So, "moving modules" is simply done by using such discovered references in each rsc/browser/ssr build entries.
scan -> rsc build -> browser build -> ssr build
scan build is not ideal though as it costs an extra rollup build. Another approach would be to "move modules" on the fly between rsc/browser/ssr builds running in parallel, but this involves tricker coordinations. See my attempt of this approach #350 and also rollup feature potentially helps this type of trick rollup/rollup#4985.
For dev, we can avoid this kind of coordination since unbundled dev can directly use "resoled id" as a reference and import(<resolve id>) can lazily process modules as needed.
Conceptually, this means that we can replace id/chunk mapping with a resolved id, which can be obtained without "moving modules" and don't need to eagerly process the actual modules on the other side of module graph / environment.
The approach assumes a following setup of multiple compilers and layers:
1st compiler with two layers for rsc and ssr
2nd compiler for browser
For the 1st compiler, it requires a intricate Webpack compilation API to achive "moving modules" between rsc and ssr layers. A following is the example of using compiler.hooks.finishMake and compilation.addModuleTree to achive this. However, this approach still has a limitation in terms of discovering "deep" references import chains, which is noted in vercel/next.js#59615.
For the 2nd compiler, client references discovered by 1st compiler can be used as browser build entries, so this step is simpler.
Turbopack
(This is mostly how I guess. I can follow up with proper details if desired.)
The end result of the output is not too different between turbopack and webpack. However, Turbopack's module context and transition concepts (in addition to webpack-style layer concept) replaces finishMake + addModuleTree-based user-land tricks with more robust first-class plugin feature. For example, this can fix webpack approach's limitation of discovering arbitrary deep reference chains (cf. vercel/next.js#59615).
For a rough picture of how Next.js uses transition, see "client reference discovery (transition example)" in #504 (though this might be out-dated).
layer features wasn't available at that time, so I made this example based on three compilers. The idea is mostly same as how I implement RSC on vite build.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
RSC intro
A following is the basic RSC rendering pipeline (see also #606)
The purpose of
react-dom/clientandreact-dom/serverdidn't change and what's added isreact tree <-> rsc streamencoding process, which allows:What this achieves is mostly similar to client island architecture, but the whole convention of uniformly writing Server/Client component tree (also with server action) is considered to have a strong DX edge.
RSC intro for bundler
For "server/client component + server action" convention, bundlers need to be able to process js modules in "separate" module graphs. A following is the example of how react libraries and user code are includes into module graphs.
Sample user code
Module graphs
Note on webpack layers
Webpack layers are not technically "separate" module graphs. Rsc and ssr module graphs above are normally included in a single server build with two layers instead.
Each js module has an layer as an additional tag for module id. For example,
reactmodule can be included in two ways (one withreact-servercondition and another without it) in a single server build. Alsoclient-component.jsxandserver-action.jsxare transformed differently in each layer.In
__webpack_modules__module factory map, each module is prefixed with(layer)as keys (see server build outputdist/server/index.cjsin https://github.com/hi-ogawa/experiments/tree/main/webpack-rsc).(ssr)./src/entry-ssr.jsx (this entry imports "(rsc)./src/entry-server.tsx") (rsc)./src/entry-server.jsx (rsc)./node_modules/react/react.react-server.js (rsc)./node_modules/react-server-dom-xxx/server.node.js (rsc)./server-component.jsx (rsc)./client-component.jsx (transformed into client references) (rsc)./server-action.jsx (ssr)./node_modules/react/index.js (ssr)./node_modules/react-server-dom-xxx/client.node.js (ssr)./node_modules/react-dom/server.node.js (ssr)./client-component.jsx (ssr)./server-action.jsx (transformed into server references)Note on client/server reference discovery
Normally bundler starts by processing server page entries (like
./server-component.jsxabove) in rsc module graph and it discovers "use client" or "use server" directives as it traverses module imports.Notably, when rsc module graph finds
"use client"directive, it needs to stop processing the actual content of the module and move it to ssr/browser module graphs. Rsc module graph only keeps track of "references" to an original modules by transforming like this:"use server"directive works in a similar way but in a reverse direction from ssr/browser module graph to rsc module graph. This can lead to arbitrary nesting of "moving modules" as described in vercel/next.js#59615.How "moving modules" is achieved in each bundler is as follows.
Vite (dev, build)
Example: https://github.com/hi-ogawa/experiments/blob/746506338b23dfc582f7c24e11980f00179aa10d/react-server-dom-vite-example/vite.config.ts
For build, it coordinates three environment builds with one extra build to scan only to discover client/server references. So, "moving modules" is simply done by using such discovered references in each rsc/browser/ssr build entries.
scan build is not ideal though as it costs an extra rollup build. Another approach would be to "move modules" on the fly between rsc/browser/ssr builds running in parallel, but this involves tricker coordinations. See my attempt of this approach #350 and also rollup feature potentially helps this type of trick rollup/rollup#4985.
For dev, we can avoid this kind of coordination since unbundled dev can directly use "resoled id" as a reference and
import(<resolve id>)can lazily process modules as needed.Conceptually, this means that we can replace id/chunk mapping with a resolved id, which can be obtained without "moving modules" and don't need to eagerly process the actual modules on the other side of module graph / environment.
const clientManifestBrowser = { - "/hashed-id-for/client-component.jsx": "/browser-build-chunk/client-component-xxx.jsx", + "/hashed-id-for/client-component.jsx": "/resolved-id-for/client-component.jsx" }However, this approach also has issues with optimize deps. The issue is demonstrated in https://github.com/hi-ogawa/rsc-tests and #379.
Webpack
The approach assumes a following setup of multiple compilers and layers:
For the 1st compiler, it requires a intricate Webpack compilation API to achive "moving modules" between rsc and ssr layers. A following is the example of using
compiler.hooks.finishMakeandcompilation.addModuleTreeto achive this. However, this approach still has a limitation in terms of discovering "deep" references import chains, which is noted in vercel/next.js#59615.Example: https://github.com/hi-ogawa/experiments/blob/746506338b23dfc582f7c24e11980f00179aa10d/webpack-rsc/webpack.config.js#L162-L174
For the 2nd compiler, client references discovered by 1st compiler can be used as browser build entries, so this step is simpler.
Turbopack
(This is mostly how I guess. I can follow up with proper details if desired.)
The end result of the output is not too different between turbopack and webpack. However, Turbopack's module context and transition concepts (in addition to webpack-style layer concept) replaces
finishMake + addModuleTree-based user-land tricks with more robust first-class plugin feature. For example, this can fix webpack approach's limitation of discovering arbitrary deep reference chains (cf. vercel/next.js#59615).For a rough picture of how Next.js uses transition, see "client reference discovery (transition example)" in #504 (though this might be out-dated).
references
Beta Was this translation helpful? Give feedback.
All reactions