React Server Components(简称 RSC)自公布以来一直是前端圈讨论的热点,它允许组件直接在服务器上执行,把渲染结果序列化后发给浏览器,客户端只需要负责交互部分。很多人以为这套方案只能在 Next.js 里用,其实它的核心是一套模块规范,Webpack 5 作为构建工具同样可以为它提供支持。本文将从原理入手,结合具体配置,讲清楚 Webpack 5 在服务器组件场景下扮演的角色。

一、Server Components 到底解决了什么问题
传统的 SSR 是把组件渲染成 HTML 字符串发给浏览器,浏览器拿到完整页面后还要再下载并执行一遍 JS 来完成水合。当组件树越来越庞大时,即使首屏很快,后续的 JS 下载与执行成本依然很高。而 Server Components 走的是另一条路:组件代码本身不进入客户端 bundle,它在服务器上执行完毕后,把渲染输出以一种特殊的序列化格式(RSC Payload)传给浏览器,客户端负责把它还原成真实的组件树。
这样做最直接的好处是体积削减。服务端组件可以直接 import 重量级依赖,比如 Markdown 解析器、日期处理库甚至 Node API,这些代码完全不会出现在浏览器端。数据获取也不再需要额外的请求往返,组件函数本身就是服务端代码,可以直接查数据库、调内部接口,拿到数据后立即渲染。
不过服务器组件有明确的边界限制:它不能使用 state、不能绑定事件、不能使用 useEffect 这类浏览器 API。一旦组件需要交互,就必须拆成客户端组件,通过 "use client" 指令标记。这套约定要求构建工具必须能识别指令、区分运行环境,这正是 Webpack 5 需要发力的地方。
二、Webpack 5 提供的构建支撑
Webpack 5 引入了 Module Federation(模块联邦),这是支撑服务器组件架构的关键能力之一。它允许多个独立构建之间共享模块,运行时按需加载。在 RSC 场景下,可以把服务端渲染部分和客户端交互部分拆成两个独立的构建产物,再通过联邦机制把它们关联起来,服务端产出的序列化流由客户端壳应用消费并还原。
另一个重要能力是 experiments 配置项。Webpack 5 的实验特性开关里包含对模块类型、顶层 await 等新语法的支持,这些是服务器组件运行的基础。同时条件导出(exports 字段中的 react-server 条件)配合 resolve.conditionNames,可以让同一份依赖在服务端和客户端构建时解析到不同的入口文件,这是区分服务器组件与客户端组件依赖的标准做法。
下面给出一个针对服务端构建的核心配置示例:
// webpack.server.config.js
const path = require('path');
module.exports = {
mode: 'development',
target: 'node', // 服务端构建,产物跑在 Node 里
entry: './src/server/index.js',
experiments: {
layers: true, // 启用 layer 机制,隔离服务端与客户端模块
},
resolve: {
conditionNames: ['node', 'import', 'react-server'], // 命中 react-server 条件导出
},
output: {
path: path.resolve(__dirname, 'dist-server'),
library: { type: 'commonjs-module' },
},
module: {
rules: [
{
test: /\.client\.jsx?$/, // 客户端组件在服务端构建中被替换为引用占位
use: path.resolve(__dirname, 'loaders/client-reference-loader.js'),
},
],
},
};
这里的关键点有两个:一是 layers 实验特性,它允许同一次构建中为不同层级的模块应用不同规则,服务端组件走真实实现,客户端组件走引用占位;二是自定义 loader,把标记了 "use client" 的文件转换成一个客户端引用模块,序列化时只输出引用 ID,不包含实现代码。
三、双端构建的组织方式与落地细节
服务器组件方案要求一次构建产出两份东西:服务端 bundle 负责执行组件并生成 RSC 流,客户端 bundle 负责还原渲染结果并接管交互。实践中通常组织成两个 Webpack 配置并行执行,共享同一份源码。客户端配置同样需要处理条件导出,只是条件名换成浏览器相关:
// webpack.client.config.js 摘要
module.exports = {
mode: 'development',
target: 'web',
entry: './src/client/index.js',
resolve: {
conditionNames: ['browser', 'import'], // 客户端命中浏览器条件
},
module: {
rules: [
{
test: /\.server\.jsx?$/, // 服务端组件在客户端构建中同样替换为占位
use: path.resolve(__dirname, 'loaders/server-reference-loader.js'),
},
{
test: /\.client\.jsx?$/,
use: 'babel-loader',
},
],
},
};
两份配置靠文件名约定或 "use client"、"use server" 指令来区分组件归属。loader 在各自的对端构建中把组件替换为引用对象,运行时通过注册表按 ID 找回真实模块。这套占位与还原机制是整个方案能跑起来的核心,也是理解 RSC bundler 工作原理的钥匙。
落地时还有几个容易踩的坑需要注意。首先是序列化限制,RSC Payload 只能携带可序列化的数据,Date 对象、Map、Set 需要特殊处理,函数无法直接跨边界传递,只能以客户端组件引用或 Server Action 的形式存在。其次是依赖兼容性,一些老库没有提供 react-server 条件导出,可能在服务端构建时把浏览器代码打包进去,此时需要用 resolve.alias 手动指定入口。最后是热更新体验,开发模式下建议配合流式传输,让服务端渲染结果边生成边推送,避免每次改动都等完整序列化完成。
四、方案取舍与总结
用 Webpack 5 手工搭建 RSC 支持能带来更细的控制粒度,模块联邦也天然适合微前端场景下的组件共享,但它需要维护两套构建配置、多个自定义 loader 和运行时协议,工程成本不低。如果只是快速上业务,Next.js 这类内置 RSC 支持的框架更省心;如果团队已有成熟的 Webpack 基础设施,或者需要在既有架构中渐进式引入服务器组件,那么基于 Webpack 5 的方案就非常值得投入。
总体来看,Webpack 5 的模块联邦、layer 机制与条件导出解析,共同构成了支撑服务器组件的技术底座。理解了双端构建中占位与还原的思路,再去读 React 官方的 RSC 规范或者 Next.js 的实现,都会顺畅很多。建议先从一个小模块开始试点,把数据密集、交互稀少的展示型组件迁到服务端,逐步体会体积与性能上的收益。
Webpack 5Server Components服务器组件修改时间:2026-09-11 14:46:44