Replies: 1 comment
-
Very usefull article!! Thank you so much!! |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
This article introduces the construction practices of RSC (React Server Components) and Server Action in React, including their concepts, rendering methods, bundling process in webpack, and how Turbopack bundles multiple environment modules in a module diagram.
A brief introduction to RSC
Before RSC, React only had Client Component, you can use the ability of Client in Client Component, including maintaining state
useState
, side effectsuseEffect
, event handling, and various APIs of Client, rendering methods There are CSR and SSR, they are all rendering of Client Component.With the launch of RSC in React, there are two components: Client Component and Server Component. Server Component can use its capabilities, including asynchronous and server-side APIs such as file systems and databases. There are also more rendering methods for Server Component.
The result of Server Component rendering will be sent to Client Component for rendering in the form of a stream, so SSR is also a client of Server Component. SSR and RSC are decoupled. You can only use RSC without SSR.
Of course, Server Component does not necessarily need a Server, it can be rendered at build time like SSG, the rendering results are stored as static files, and may even be rendered by Worker, but the intention of React under RSC architecture is to combine Server.
Strongly recommend React official this RFC: React Server Component, written very clearly and comprehensively, of course, the integration part (framework integration, Router integration, Bundler integration, etc.) is written relatively simply, so this document elaborates on how to integrate with Bundler.
In addition to Server Component, React has also launched Server Action, which is still in the exploration stage and has no RFC. It is very convenient to use it to call the server API. It strongly depends on the Server, and the implementation is decoupled from the Server Component. When only using Client Component, Server Action can also be used.
Server Action will compile into an async function that calls the server endpoint during compilation, which means it is composable. You can use it as a queryFn of ReactQuery.
Bundling by webpack
RSC
According to the RFC, RSC's requirements for bundlershave the following four points:
"use client"
and treat them as Client modules."react-server"
exports in package.json."use client"
) will be used as a potential code splitting point (Code Splitting)The first two points are relatively easy to understand.
"use client"
is the directive of the Client boundary. Within the boundary, the module introduced by the"use client"
directive will be used as the Client module regardless of whether there is a"use client"
directive. Outside the boundary, it defaults to the Server module, and the exported components of the Client module will be treated as Client Components. When bundling, we will parse the AST in a webpack loader to identify the"use client"
directive."react-server"
will introduce the corresponding file of the exports when resolving, some libraries that adapt to RSC need to specify the exports in package.json. When bundling, only need to add the resolve condition inmodule.rule[].resolve
to achieve this.The last two points mainly help React to load Client Components at runtime. But first, let's introduce the overall rendering process and the concept of Reference.
Rendering process
The Client module containing
"use client"
in the Server environment will not retain the original content, but will be replaced by Client References:will be replaced to:
When the Server renders the Server Component, the imported Client Component will actually be a Client Reference. The React runtime will get its exports, ChunkGroup chunks, module id and other metadata in the manifest through the path information on the Reference, and then generate the serialized JSX:
When sent to the Client (including SSR) to render this Server Component result (serialized JSX), when encountering Client Reference, the Client module will be loaded through these metadata. First, React will load chunks by
__webpack_chunk_load__(chunkId)
, and then run the corresponding module by__webpack_require__(moduleId)
, and finally get the exported client component for rendering.RSC-only
Let's start with RSC-only, forget about SSR for now. We will have two compiler, onc for server environment (
target: "node"
) and one for client environment (target: "web"
), our application is also divided into two entries,client-entry.js
andserver-entry.js
.client-entry.js
will mount the Root component, but does not render the main App component, because App is a Server Component, which will be re-exported inserver-entry.js
, bundled as a library, introduced and rendered inserver.js
(server.js
is a dev/prod server and will not be bundled by the server compiler),createFromFetch
will send a network request to fetch the serialized JSX and render it on the client.We assume our application's module graph is like this:
In the server compile, when compiling the
ClientComp.js
module, we will encounter"use client"
, we need a webpack loader to handle it, This Loader needs to parse AST to identify"use client"
and collect all exports to replace it with Client Reference. There is no longer a connection toChild.js
in Client Reference, so our server compile process ends, and then we enter the client compile, this is also conforms to the definition of"use client"
as a boundary.For the real Client module
ClientComp.js
, we will collect the paths of these dependencies in the server compile. In the client compile, we will use theAsyncDependenciesBlock
as the code splitting point, and add these dependencies as the dependencies of the RSC client runtime (react-server-dom-webpack/client
). Then we will have the following Module Graph:At the same time, these dependencies' metadata will be collected in the client compile, such as the module id mentioned above, chunks in the ChunkGroup, export variable names, etc., to generate a corresponding client-side rendering manifest (
client-modules.json
) for rendering the Client Reference in the server.CSS
We don't consider the runtime css-in-js solution here. Currently, the runtime css-in-js can only be used in the
"use client"
module (some buildtime css-in-js solutions, such as pandaCSS, can be used in Server Component). Here we only consider how to import a global style (import "./global.css"
) in Server Component.If we introduce CSS in the Client module within the
"use client"
boundary, this part of CSS will be moved into the client compile together with the Client modules and go through the normal client compilation without any problem. However, if CSS is introduced from a Server module likeApp.js
, this part of CSS will not enter the client compilation, and then problems will arise. Therefore, we also need a way to move this part of CSS into the client's compilation process.Actually, it's quite simple. CSS modules can be understood as implicit
"use client"
modules. The only difference is that CSS modules don't need to be loaded asynchronously as code splitting points. Moreover, Server modules must be the parent modules of Client modules. The rendering of Server Components will precede that of Client Components. Therefore, CSS modules can directly serve as ModuleDependencies ofreact-server-dom-webpack/client
and can be loaded on the first screen.SSR
Next, let's add the Server-Side Rendering (SSR) feature. SSR is the pre-rendering of Client Components on the server side. All you need to do is add the Client Components to the server compile and bundle the output of the Client Components that can run in the server environment. However, there are currently two problems.
ClientComp.js
, the content will be replaced with Client Reference. Since there is no introduction of the sub-moduleChild.js
, the server compile directly ends the construction of the module graph in the make stage. So how can we add the realClientComp.js
and its sub-modules to the module graph?ClientComp.js
is replaced with Client Reference, only the content has changed, and the unique identifier of the module remains unchanged. If we want to add the realClientComp.js
module to the module graph as well, how can we ensure that its identifier does not conflict with that of the Client Reference module?For the first problem, we can construct a new virtual Entry module from the collected
"use client"
Client boundary modules during thefinishMake
hook. The content of this Entry isimport(/* webpackMode: "eager" */ "ClientComp.js")
. Here, using dynamic import can ensure that in the production environment, there will be no errors in the output caused by tree-shaking and module concatenation optimizations. UsingwebpackMode: "eager"
ensures that it won't be code-split. After that, we call theaddModuleTree
method (which is also the underlying API used by the EntryPlugin when processing the entry in the normal config) to add this Entry module to the build queue and start the second round of make.For the second problem, it can be achieved by using
layer
.Layer
is an API provided by webpack that can divide the same module into multiple "clones" in the same compilation process. Different "clones" with differentlayer
will have different identifiers.Layer
can be configured throughmodule.rule[].layer
. The defaultlayer
of a module will inherit thelayer
of its issuer (the module that first imports it). In fact, the Client Component in SSR is also the client part of the Server Component. So we mark the original Server Component as theserver layer
, and mark the Client Component added by callingaddModuleTree
as theclient layer
. Then we can get the following Module Graph:In order to render the Client Component during Server-Side Rendering (SSR), similarly, we also need to generate a corresponding manifest (
client-modules-ssr.json
) for SSR rendering.HMR
The processing of Client Components is the same as that of Client-Side Rendering (CSR). Just insert the Hot Module Replacement (HMR) runtime and use
react-refresh/babel
to compile and insert the runtime into each Client module.The processing of Server Components is a bit different. Since the Server Components are rendered through fetch requests and then using the results of those requests, the updates of Server Components, including those caused by code modifications in the development environment and those caused by event handling, route jumps, etc. in the production environment, only require refetching.
Server Action
Currently, there are three ways to define Server Action:
"use server"
is added at the function level, then this async function will be regarded as a Server Action."use server"
is added at the top level of this module, then the async function exported by this module will be regarded as a Server Action."use server"
is added at the top level of this module, then the async function exported by this module will be regarded as a Server Action.The first two ways can be understood as being introduced by the server environment since they are either defined in the Server module itself or imported by the Server module. The third way, which is introduced by the Client module, can be understood as being introduced by the client environment. Based on this, Server Action can be divided into two types: from server and from client. The calling process and bundling methods of these two types of Server Action are also different.
Calling process of "from client" Server Action
Firstly, let's introduce the calling process of the Server Action of "from client".
Similar to
"use client"
, the output of the module containing"use server"
in the Client environment will not retain the original content but will be replaced with Server References instead.will be replaced to:
Therefore, when the
handleSubmit
function is called, thecallServer
will be invoked to make a fetch request to the server side.When
callServer
is called, the id of the Server Reference will be passed toserver.js
through thersc-action
header field. Inserver.js
, based on the manifest that records the metadata of Server Action, the real Server module (actions.js
) will be found and loaded among the products of server compilation. Then, the corresponding async function will be retrieved according to the export name, and it will be called on the Server side and the result will be responded.Bundling of "from client" Server Action
Similar to the handling of the bundling of Client Components in RSC, the webpack loader is used to replace the content of the
"use server"
module with Server References. The"use server"
modules are collected, and a third round of "make" is carried out throughaddModuleTree
, and then marked back to the server layer. Meanwhile, the metadata is collected to generate a manifest (server-actions.json
), which includes the module id, the name of the exported variable, and the chunks in the ChunkGroup where it is located.Taking client compile into account, the following Module Graph can be obtained:
Calling process of "from server" Server Action
Since this kind of Server Action is inherently in the server environment and is passed to the Client Component through props, when this kind of Server Action is called, React cannot obtain the called id in the Server Reference in a way similar to that of "from client". Instead, when rendering the RSC, this id has to be serialized along with the Server Component's JSX and then transmitted to the client side over the network.
When it is called, the id will be retrieved from the props of the rendered Client Component, and then a request will be sent to the server side via
callServer
. After that, the process will be the same as that of the "from client" case.Bundling of "from server" Server Action
When bundling this kind of Server Action, it only needs to add some metadata, including the id and the export name, to the exports of the module through the webpack loader. When the RSC renders the props of the Client Component and encounters the Server Action, it will read this metadata and then render the corresponding serialized content.
will be transform into something like this:
How to Bundling in One Compilation
Judging from the above compilation process of Webpack, it requires server compile and client compile to conduct two compilations for the server environment and the client environment respectively. So, is there a way to complete the bundling for different environments in just one compilation?
In fact, there is a similar implementation in Webpack: Worker. During compilation, it will switch from the web/node environment to the webworker environment. The core API is
AsyncEntrypoint
. Eachnew Worker()
will correspondingly create anAsyncEntrypoint
and attach the runtime modules required by the webworker environment. Its model is somewhat different from that of RSC, but theoretically, it is possible to useAsyncEntrypoint
to achieve bundling of RSC in one compilation by webpack.Next, let's see how Tobias achieved bundling of RSC in one compilation in Turbopack.
Switching Environments
In Turbopack, one of the core concepts is Context, which is divided into
AssetContext
andChunkingContext
. It can be understood as the underlying abstraction of Webpack configuration. AssetContext corresponds to some configurations in the make phase, while ChunkingContext corresponds to some configurations in the seal phase.Different modules can have different AssetContexts. The AssetContext of modules can be changed through Transition. For example, JavaScript modules and CSS modules use different AssetContexts to implement different resolve logics, use different AssetContexts to generate modules of different layers, and modules belonging to different environments use different AssetContexts to generate products for different environments.
Different chunks can have different ChunkingContexts. Currently, they are divided into
DevChunkingContext
andBuildChunkingContext
. For example, in development mode and production mode, different ChunkingContexts are used to implement different bundling strategies, insert different runtime codes, and control the paths of the products.Turbopack changes the environmental information of modules by changing AssetContext. Then it calls
chunk_group
/evaluated_chunk_group
, taking this module as the entry module to generate the entry chunk, and then generates ChunkGroup/Entrypoint. According to the environmental information of the entry module, products for different environments are thus generated (actually, it can also be seen from this that a significant difference between Turbopack and Webpack is that Turbopack is pull-based while Webpack is push-based, including the generation of Module Graph and Chunk Graph, as well as the implementation of incremental compilation).In this way, Turbopack achieves the switching of environments and also meets the third condition for RSC bundling, serving as a code splitting point, that is, generating ChunkGroup from the client boundary module.
Manifest
The second condition for RSC bundling is the ability to provide sufficient metadata to enable the client side to request and load the Client Component based on the serialized JSX (Server Component render results). In the current development mode, Turbopack does not write these metadata into a separate manifest file. Instead, it uses a special module. It can be simply understood that this metadata is included in the generated Client Reference module (of course, this requires some changes to the way the React runtime reads this information). This is not only the case for the manifest required by RSC, but also for the manifest of entry chunks required for SSR rendering.
Bundling
Next, let's briefly go through the bundling process of Turbopack. We'll start from the following initial module graph (for the sake of easy understanding, there are some simplifications to the actual implementation).
Firstly, start bundling from
server.js
, which is the server-entry (since the environment can be switched, there is no need to bundle it as a library and re-export modules likeApp.js
anymore). When bundling encountershydrate.js
, which is the client-entry and a dependency of the server-entry, this module serves as the entry for the client and performs operations like hydration. It belongs to the client environment, so we use Transition to switch this module to the client environment. Meanwhile, since it is the Entrypoint for the client andserver.js
needs all the script paths of this Entrypoint when handling the"/"
request for the first-screen SSR rendering, we add a special module and generate the paths of these chunks when generating its code. And when generating the code for the Entrypoint, it should be attached with the runtime code required by the client side. We useevaluated_chunk_group
to bundle this module and its dependencies as an Entrypoint.After that, continue with the compilation. When encountering
ClientComp.js
, after recognizing"use client"
during parsing, start the Transition and switch to the compilation environment on the client side. Then add the"react-server"
resolve condition, and generate the Client Reference module and relevant meta-information. Meanwhile, sinceClientComp.js
will serve as a code splitting point, we usechunk_group
to bundle this module and its dependencies as a ChunkGroup.Based on this, we obtain the following Module Graph:
Here is a more complete diagram that summarizes and compares the two compilation methods:
Advantages
There may be some advantages compared to two compilations. Because in the same compilation, more information about modules in other environments can be obtained, such as the used exports (usedExport), so that tree-shaking can be done on the unused exports. However, in the case of two compilations, since this information is lost (it may be possible to save this information through a manifest), in order to ensure that the used exports are not tree-shaked, tree-shaking should not be done on all exports (
setUsedInUnknownWay
).Of course, not all scenarios are suitable for completing bundling in one compilation. For RSC, where some modules run on the server and some modules run on the client, and even in next.js there are some middleware modules that will run on the server, it is quite suitable. But for SSR, where basically all modules run both on the server and on the client, it is not so necessary.
Beta Was this translation helpful? Give feedback.
All reactions