🔗 原文链接:Making Sense of React Server Components
👨💻 原作者:Josh W. Comeau
📅 发布时间:2023年9月6日,最后更新:2025年5月9日
⚠️ 关于本译文
本文基于 Josh W. Comeau 的原文进行忠实翻译,力求准确传达原作者的技术观点和逻辑结构。
🎨 特色亮点:
- 保持原文的完整性和技术准确性
- 采用自然流畅的中文表达,避免翻译腔
- 添加画外音板块,提供译者的补充解读和实践心得
- 使用生动比喻帮助理解复杂概念
💡 画外音说明: 文中标注为画外音的部分是译者基于实际开发经验添加的拓展解释,旨在帮助读者更好地理解和应用这些概念,不代表原作者观点。
🖼️ 关于交互式示例: 本文中的图表和交互式演示以占位符形式呈现。如需体验完整的交互式功能,可前往原文进行实际操作。
🚀 简介
说一件让我感觉自己变老的事:React 今年迎来了它的 10 岁生日!
在 React 首次亮相并让开发者社区一脸懵逼的这十年里,它经历了好几次重大演变。React 团队在激进变革方面从不手软:只要他们发现了更好的解决方案,就会毫不犹豫地推进。
几个月前,React 团队揭开了 React Server Components 的神秘面纱——这是最新的颠覆性变革。有史以来第一次,React 组件可以只在服务器上运行了。
网上对这个东西的困惑简直太多了。大家有一堆问题:这玩意儿到底是什么?怎么工作的?有什么好处?它和服务端渲染(Server Side Rendering)又是什么关系?
我已经对 React Server Components 做了大量实验,也解答了自己的很多疑惑。老实说,我对这东西的兴奋程度远超预期。它真的很酷! ✨
所以今天,我的目标是帮你揭开这层神秘面纱,解答你可能对 React Server Components 的种种疑问!
📖 目标读者
本教程主要面向已经在使用 React 的开发者,以及对 React Server Components 感到好奇的人。你不需要是 React 专家,但如果你刚开始学 React,可能会觉得有些困惑。
🔍 服务端渲染快速入门
为了把 React Server Components 放到正确的语境中理解,先搞清楚**服务端渲染(SSR)**是如何工作的会很有帮助。如果你已经熟悉 SSR,可以直接跳到下一节!
2015 年我刚开始用 React 的时候,大多数 React 项目都采用"客户端渲染"策略。用户收到的 HTML 文件长这样:
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
那个 bundle.js 脚本包含了挂载和运行应用所需的一切:React 本身、其他第三方依赖,还有我们写的所有代码。
一旦 JS 下载并解析完成,React 就会开始施展魔法,为整个应用生成所有 DOM 节点,然后塞进那个空的 <div id="root"> 里。
这种方式的问题是:这一切都需要时间。而在这段时间里,用户只能盯着一片白花花的空白屏幕。更糟糕的是,这个问题往往会随时间恶化:我们每发布一个新功能,JavaScript 包就会增加几 KB,用户等待的时间也就越来越长。
服务端渲染就是为了改善这种体验而诞生的。 服务器不再发送一个空的 HTML 文件,而是先把我们的应用渲染一遍,生成真正的 HTML。用户收到的是一个完整的 HTML 文档。
这个 HTML 文件仍然会包含 <script> 标签,因为我们还是需要 React 在客户端运行来处理交互。但我们会配置 React 在浏览器里以稍微不同的方式工作:它不会从零开始生成所有 DOM 节点,而是"接管"现有的 HTML。这个过程叫做水合(hydration)。
我很喜欢 React 核心团队成员 Dan Abramov 对此的解释:
水合就像是用"交互性和事件处理器"这股"活水",去浇灌"干巴巴的" HTML。
一旦 JS 包下载完成,React 会快速遍历我们的整个应用,构建起 UI 的虚拟草图,然后把它"嵌合"到真实的 DOM 上,绑定事件处理器,触发各种 effect,等等。
总之,这就是 SSR 的核心思想。 服务器生成初始 HTML,这样用户就不必在 JS 包下载和解析期间盯着空白页发呆。客户端的 React 接过服务端 React 的接力棒,接管 DOM 并注入交互性。
🔔 画外音
这里的"水合"比喻真的很形象!你可以想象服务端渲染的 HTML 就像是一株干枯的植物标本,虽然看起来有模有样,但没有生命。而水合就是给这株植物浇水,让它"活"过来,能够响应用户的点击、输入等操作。
🌂 一个统称
当我们谈论服务端渲染时,通常会想到这样的流程:
- 用户访问 myWebsite.com
- Node.js 服务器收到请求,立即渲染 React 应用,生成 HTML
- 这份新鲜出炉的 HTML 被发送给客户端
这是实现服务端渲染的一种方式,但不是唯一的方式。另一种选择是在构建应用时就生成 HTML。
通常,React 应用需要经过编译,把 JSX 转换成普通的 JavaScript,并打包所有模块。那如果在同一个过程中,我们把所有路由的 HTML 都"预渲染"出来呢?
这就是常说的静态站点生成(SSG)。它是服务端渲染的一个子变体。
在我看来,"服务端渲染"是一个统称,涵盖了几种不同的渲染策略。它们都有一个共同点:初始渲染发生在像 Node.js 这样的服务器运行时中,使用
ReactDOMServerAPI。至于这个渲染发生在什么时候——是按需进行还是在编译时进行——其实并不重要。不管怎样,都算服务端渲染。
🔄 来回折腾
我们来聊聊 React 中的数据获取。通常,我们会有两个通过网络通信的独立应用:
- 客户端 React 应用
- 服务端 REST API
使用 React Query、SWR 或 Apollo 之类的库,客户端向后端发起网络请求,后端从数据库获取数据,再通过网络返回。
我们可以用一张图来可视化这个流程:
关于这些图表的说明
这篇博文包含了几张"网络请求图"。它们旨在可视化数据如何在客户端(浏览器)和服务器(后端 API)之间流动,跨越几种不同的渲染策略。
底部的数字代表的是虚构的时间单位,不是分钟或秒。实际上,这些数字会根据很多不同因素而变化。这些图只是为了让你对概念有一个高层次的理解,并不是在建模任何真实数据。
第一张图展示的是使用**客户端渲染(CSR)**策略的流程。一开始,客户端收到一个 HTML 文件。这个文件没有任何内容,但有一个或多个 <script> 标签。
一旦 JS 下载并解析完成,我们的 React 应用就会启动,创建一堆 DOM 节点并填充 UI。但一开始,我们没有任何实际数据,所以只能渲染一个"骨架"(页头、页脚、大致布局)加一个加载状态。
你可能见过很多这种模式。比如,UberEats 一开始会先渲染骨架,同时获取填充实际餐厅所需的数据:
用户会一直看到这个加载状态,直到网络请求完成、React 重新渲染,用真实内容替换加载 UI。
我们来看看另一种架构方式。 下一张图保持相同的数据获取模式,但使用服务端渲染而不是客户端渲染:
在这个新流程中,我们在服务器上执行第一次渲染。这意味着用户收到的 HTML 文件不再是完全空的。
这是一个进步——一个骨架总比白屏好——但说实话,它并没有真正带来质的改变。用户来我们的应用不是为了看加载画面的,他们是来看内容的(餐厅、酒店列表、搜索结果、消息,随便什么)。
为了真正感受用户体验的差异,让我们给图表加上一些 Web 性能指标。在这两种流程之间切换,注意那些标记的变化:
每个标记代表一个常用的 Web 性能指标。以下是详细说明:
- First Paint(首次绘制)—— 用户不再盯着白屏了。大致布局已经渲染出来,但内容还没有。有时也叫 FCP(First Contentful Paint,首次内容绘制)。
- Page Interactive(页面可交互)—— React 已经下载完成,我们的应用已经渲染/水合。交互元素现在完全可响应了。有时也叫 TTI(Time To Interactive,可交互时间)。
- Content Paint(内容绘制)—— 页面现在包含了用户真正关心的东西。我们已经从数据库拉取数据并在 UI 中渲染出来了。有时也叫 LCP(Largest Contentful Paint,最大内容绘制)。
通过在服务器上进行初始渲染,我们能更快地绘制出那个初始"骨架"。这可以让加载体验感觉快一些,因为它提供了一种进展感——事情正在进行中。
在某些情况下,这确实是一个有意义的改进。比如,也许用户只是在等页头加载出来,好点击一个导航链接。
但这个流程是不是感觉有点傻? 当我看 SSR 图时,我忍不住注意到:请求是从服务器开始的。既然如此,为什么不在那个初始请求中就完成数据库查询,而非要再来一次网络往返呢?
换句话说,为什么不这样做?
与其在客户端和服务器之间来回折腾,我们直接把数据库查询作为初始请求的一部分,把完全填充好的 UI 直接发送给用户。
但是,具体要怎么实现呢?
要让这个方案可行,我们需要能够给 React 一段只在服务器上运行的代码,来执行数据库查询。但 React 一直没有这个选项……即使是服务端渲染,我们的所有组件也都会同时在服务器和客户端渲染。
生态系统已经为这个问题想出了很多解决方案。像 Next.js 和 Gatsby 这样的元框架(Meta-frameworks)创建了各自的方式来在服务器上独占运行代码。
比如,使用 Next.js(旧版的 Pages Router)是这样的:
import db from 'imaginary-db';
// 这段代码只在服务器上运行:
export async function getServerSideProps() {
const link = db.connect('localhost', 'root', 'passw0rd');
const data = await db.query(link, 'SELECT * FROM products');
return {
props: { data },
};
}
// 这段代码在服务器和客户端都运行
export default function Homepage({ data }) {
return (
<>
<h1>Trending Products</h1>
{data.map((item) => (
<article key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</article>
))}
</>
);
}
让我们来拆解一下:当服务器收到请求时,getServerSideProps 函数被调用。它返回一个 props 对象。这些 props 随后被传入组件,组件先在服务器上渲染,然后在客户端水合。
这里的巧妙之处在于,getServerSideProps 不会在客户端重新运行。事实上,这个函数甚至不会被包含在我们的 JavaScript 包里!
这种方式在当时非常超前。说实话,它真的很棒。但也有一些缺点:
- 这种策略只能在路由级别使用,只适用于组件树最顶层的组件。我们不能在任意组件中这样做。
- 每个元框架都有自己的方案。Next.js 一套,Gatsby 一套,Remix 又是一套。没有标准化。
- 我们所有的 React 组件都会在客户端水合,即使它们根本不需要这样做。
多年来,React 团队一直在悄悄琢磨这个问题,试图找到一个官方的解决方案。他们的答案就是 React Server Components。
🔔 画外音
这里作者点出了一个很关键的问题:传统的数据获取模式需要"来回折腾",即使用了 SSR,我们仍然需要在客户端发起额外的请求来获取数据。这就是 RSC 要解决的核心痛点之一。
🎯 React Server Components 简介
简单来说,React Server Components 代表了一种全新的玩法。在这个新世界里,我们可以创建专门在服务器上跑的组件。这意味着我们能做一些以前想都不敢想的事——比如直接在 React 组件里写数据库查询!你喜欢哪个风格?我帮你替换。
下面是一个"Server Component"的简单示例:
import db from 'imaginary-db';
async function Homepage() {
const link = db.connect('localhost', 'root', 'passw0rd');
const data = await db.query(link, 'SELECT * FROM products');
return (
<>
<h1>Trending Products</h1>
{data.map((item) => (
<article key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</article>
))}
</>
);
}
export default Homepage;
作为一个写了好多年 React 的人,我第一次看到这段代码时整个人都傻了。😅
"等等!"我脑子里警铃大作。"函数组件怎么能是 async 的?!而且直接在渲染里写副作用,这不是犯规吗?!"
理解下面这点非常关键:Server Components 永远不会重新渲染。它们只在服务器上跑一次,生成 UI,然后渲染结果就被发送到客户端并"锁死"了。在 React 看来,这个输出就像被浇筑成混凝土一样,永远不会变。
这意味着 React 的很多 API 在 Server Components 里都用不了。比如不能用 state——因为 state 会变,但 Server Components 压根不会重新渲染。也不能用 effects——因为 effects 是在渲染之后、在客户端才跑的,而 Server Components 永远到不了客户端。
不过换个角度想,这也意味着我们可以更自由一些。在传统 React 里,我们得把副作用塞进 useEffect 或者事件处理器里,免得每次渲染都重复执行。但如果组件就只跑一次,那还担心什么呢!
Server Components 本身其实挺简单的,但"React Server Components"这整套玩法就复杂多了。因为我们还是有传统的老式组件在,而它们怎么搭配在一起,确实挺让人头大的。
在这个新体系里,我们熟悉的那些"传统" React 组件现在叫做 Client Components(客户端组件)。说实话,我不太喜欢这个名字。😅
"Client Component"这名字听起来好像它们只在客户端渲染似的,但其实不是这样。Client Components 是在客户端和服务器两边都会渲染的。
我知道这些术语挺绕的,所以我来总结一下:
- React Server Components 是这整套新玩法的名字。
- 在这套新体系里,我们以前熟悉的"普通"React 组件被改名叫 Client Components。换了个新名字,东西还是那个东西。
- 这套新体系引入了一种全新的组件类型:Server Components。这些新组件只在服务器上渲染,它们的代码不会被打包进 JS 里,所以永远不会水合,也不会重新渲染。
React Server Components 🆚 服务端渲染
这里要澄清另一个常见的误解:React Server Components 不是用来替代服务端渲染的。别把它当成"SSR 2.0"。
我更喜欢把它们想成两块完美契合的拼图,两种相互搭配的调味料。
我们仍然靠服务端渲染来生成最初的 HTML。React Server Components 是在这个基础上锦上添花——让我们能把某些组件从客户端的 JS 包里剔除,确保它们只在服务器上跑。
事实上,你甚至可以在不用服务端渲染的情况下使用 RSC,不过实际操作中两者配合效果最佳。React 团队做了一个不用 SSR 的极简 RSC 演示,有兴趣可以看看。
🔔 画外音
这一点非常重要!很多人把 RSC 和 SSR 混为一谈,或者以为 RSC 是 SSR 的升级版。实际上它们是两个不同的概念,解决不同的问题。SSR 解决的是"首屏渲染速度"问题,而 RSC 解决的是"组件在哪里运行"的问题。把它们结合使用,才能发挥最大威力。
🔧 兼容环境
通常,React 出了什么新特性,我们只要升级一下依赖版本就能用了。跑个 npm install react@latest,完事儿。
可惜,React Server Components 不是这么玩的。
据我理解,RSC 需要跟 React 之外的一大堆东西深度配合——打包器、服务器、路由器,都得改。
截至我写这篇文章时,想用 RSC 只有一条路:用 Next.js 13.4+,而且得用它们重新设计的"App Router"。
希望以后会有更多框架支持 RSC。一个 React 核心特性只能在一个框架里用,这也太别扭了!React 官方文档有个"前沿框架"列表,我打算时不时去瞅一眼,看看有没有新选择。
📝 声明客户端组件
在这套新体系里,所有组件默认都是 Server Components。想用 Client Components,得主动"申请"。
具体做法是在文件顶部加一行特殊指令:
'use client';
import React from 'react';
function Counter() {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Current value: {count}
</button>
);
}
export default Counter;
顶部那个孤零零的字符串 'use client',就是在告诉 React:"这个文件里的组件是 Client Components,请把它们打包进 JS,这样才能在客户端重新渲染。"
这种写法看起来挺奇怪的,但其实有先例——JavaScript 的 "use strict" 也是这个套路。
注意,Server Components 不需要写 'use server'——在 RSC 体系里,没标注的组件默认就是 Server Components。'use server' 其实是给 Server Actions 用的,那是另一个功能,本文就不展开了。
🤔 怎么判断该用哪种组件?
你可能在想:我怎么决定一个组件该是 Server Component 还是 Client Component?
经验法则是:能用 Server Component 就用 Server Component。Server Components 通常更简单、更好理解。而且还有性能加成:Server Components 不在客户端跑,代码也不会被打包进 JS 里。RSC 的一大好处就是能优化 Page Interactive(TTI)时间。
话虽如此,也别把"消灭所有 Client Components"当成人生目标!不要为了追求最少的 Client Components 而过度优化。别忘了,在此之前,每个 React 应用里的每个组件都是 Client Component。
真正上手后你会发现这其实很直观:有些组件需要用 state 或者 effects,那就加上 'use client';其他的就让它们保持 Server Components。就这么简单。
🔔 画外音
这里作者给出了一个很实用的判断标准:需要用到 state、effects 或者浏览器 API 的组件,才需要是 Client Component。其他的都可以(也应该)保持为 Server Component。别过度优化,保持简单就好!
🚧 边界问题
我刚接触 RSC 时,脑子里蹦出的第一个问题是:props 变了怎么办?
比如说,有这么一个 Server Component:
function HitCounter({ hits }) {
return (
<div>
Number of hits: {hits}
</div>
);
}
假设首次服务端渲染时,hits 是 0,组件会输出:
<div>
Number of hits: 0
</div>
那如果 hits 变了呢?比如它是个 state,从 0 变成了 1。HitCounter 需要重新渲染——但它是 Server Component,重新渲染不了啊!
问题在于,Server Components 不能孤立地看。我们得拉远镜头,从整个应用结构来考虑。
假设我们有这样一棵组件树:
如果所有组件都是 Server Components,那没问题——没人会重新渲染,props 自然也不会变。
但假设 Article 组件需要管理 hits 这个 state。要用 state,就得把它变成 Client Component:
看出问题了吗?Article 重新渲染时,它下面的 HitCounter 和 Discussion 也得跟着重新渲染。但如果它们是 Server Components,就渲染不了。
为了避免这种死局,React 团队加了一条规则:Client Components 只能导入其他 Client Components。一旦给 Article 加上 'use client',HitCounter 和 Discussion 也得变成 Client Components。
我在学 RSC 时最大的"原来如此"时刻,就是意识到这套新玩法的核心其实是划定客户端边界。实际情况是这样的:
当我们给 Article 加上 'use client' 时,就创建了一个"客户端边界"。这个边界里的所有组件都会被自动"感染"成 Client Components——哪怕 HitCounter 本身没写 'use client',在这种情况下它也会在客户端渲染和水合。
好消息是:我们不用给每个需要在客户端跑的文件都加 'use client'。实际操作中,只需要在创建新边界时加就行了。
🔔 画外音
"客户端边界"这个概念非常关键!你可以把它想象成一道"分水岭":一旦某个组件被标记为 Client Component,它下面导入的所有组件都会"感染"成 Client Component。理解这一点对于正确设计组件结构至关重要。
🛠️ 巧妙的变通方法
当我第一次听说"Client Components 不能渲染 Server Components"时,心想这限制也太死了吧。万一我需要在应用顶层用 state 呢?难道所有东西都得变成 Client Component??
事实证明,很多情况下我们可以调整一下组件结构来绕过这个限制。
说起来有点绕,还是看例子吧:
'use client';
import { DARK_COLORS, LIGHT_COLORS } from '@/constants.js';
import Header from './Header';
import MainContent from './MainContent';
function Homepage() {
const [colorTheme, setColorTheme] = React.useState('light');
const colorVariables = colorTheme === 'light'
? LIGHT_COLORS
: DARK_COLORS;
return (
<body style={colorVariables}>
<Header />
<MainContent />
</body>
);
}
这里我们需要用 state 来实现深色/浅色模式切换。因为要把 CSS 变量加到 <body> 上,所以这个 state 得放在组件树的顶层。
问题是,要用 state 就得让 Homepage 变成 Client Component。而它是应用的顶层,这意味着 Header 和 MainContent 也会被自动"感染"成 Client Components。
解决办法是:把主题管理的逻辑抽出来,单独放一个文件:
// /components/ColorProvider.js
'use client';
import { DARK_COLORS, LIGHT_COLORS } from '@/constants.js';
function ColorProvider({ children }) {
const [colorTheme, setColorTheme] = React.useState('light');
const colorVariables = colorTheme === 'light'
? LIGHT_COLORS
: DARK_COLORS;
return (
<body style={colorVariables}>
{children}
</body>
);
}
然后在 Homepage 里这样用:
// /components/Homepage.js
import Header from './Header';
import MainContent from './MainContent';
import ColorProvider from './ColorProvider';
function Homepage() {
return (
<ColorProvider>
<Header />
<MainContent />
</ColorProvider>
);
}
现在 Homepage 不用 state 了,可以把 'use client' 去掉。这样 Header 和 MainContent 就不会被感染成 Client Components 了!
等等,但是…… ColorProvider 是 Client Component,而且它是 Header 和 MainContent 的父组件啊?从树结构来看,它不是在更上面吗?
关键来了:判断客户端边界时,父子关系不重要。重要的是谁在"导入"和"渲染"这些组件。Homepage 才是导入和渲染 Header、MainContent 的那个组件,所以是 Homepage 在决定它们的 props。
回想一下,我们要解决的问题是:Server Components 不能重新渲染,所以它们的 props 不能变。在新的结构里,Header 和 MainContent 的 props 是由 Homepage(一个 Server Component)决定的,自然就没问题了。
我承认这真的很烧脑。 就算写了好多年 React,我还是觉得这块挺绕的 😅。得多练几次才能建立起直觉。
更准确地说,'use client' 是在文件/模块级别生效的。Client Component 文件里导入的任何模块,也必须是 Client Components——毕竟打包器会顺着 import 一路追下去嘛!
🔔 画外音
这个技巧的核心思想是:通过
childrenprop 传递 Server Components,而不是在 Client Component 中直接导入它们。这样,Server Components 的"所有权"就保留在了它们的父 Server Component 那里,而不会被 Client Component "感染"。这是一个非常实用的模式!
怎么切换主题?
你可能注意到了,上面的例子里没法真的切换主题——
setColorTheme从来没被调用过。我是想让例子尽量简洁才省略的。完整版会用 React Context 把 setter 函数传给子组件。只要消费 context 的组件是 Client Component,一切都能正常工作!
🔬 掀开引擎盖看看
让我们更深入地看看:使用 Server Component 时,输出的到底是什么?
从最简单的 React 应用开始:
function Homepage() {
return (
<p>
Hello world!
</p>
);
}
在 RSC 体系里,组件默认都是 Server Components。既然我们没给它加 'use client'(也没把它放在客户端边界里),它就只会在服务器上渲染。
当我们在浏览器里打开这个应用,收到的 HTML 大概长这样:
<!DOCTYPE html>
<html>
<body>
<p>Hello world!</p>
<script src="/static/js/bundle.js"></script>
<script>
self.__next['$Homepage-1'] = {
type: 'p',
props: null,
children: "Hello world!",
};
</script>
</body>
</html>
免责声明
为了便于理解,我对结构做了简化。真实的 RSC 输出用的是字符串化的 JSON 数组,目的是减小文件体积。
我也去掉了 HTML 里不重要的部分(比如
<head>)。
我们看到 HTML 里有 React 应用生成的 UI——那个 "Hello world!" 段落。这是服务端渲染的功劳,跟 RSC 没有直接关系。
下面有个 <script> 标签加载 JS 包。这个包里有 React 等依赖,还有应用里的 Client Components。但因为 Homepage 是 Server Component,它的代码不在这个包里。
最后还有一段内联 JS:
self.__next['$Homepage-1'] = {
type: 'p',
props: null,
children: "Hello world!",
};
这才是真正有意思的部分。 我们在告诉 React:"嘿,我知道你拿不到 Homepage 的代码,但别慌——这是它渲染出来的结果。"
通常 React 在客户端水合时,会快速把所有组件跑一遍,建立起应用的虚拟表示。但对于 Server Components,它做不到这一点——因为代码根本没打包进来。
所以我们直接把渲染结果发过去,也就是服务器生成的那个虚拟表示。React 在客户端加载时,直接用这个现成的描述,不用自己再算一遍。
这就是前面 ColorProvider 例子能工作的原因。 Header 和 MainContent 的输出通过 children 传给 ColorProvider。ColorProvider 想怎么重新渲染都行,但这些数据是静态的,被服务器锁死了。
这确实意味着:JS 包变小了,但 HTML 变大了。组件定义没了,换成了组件的返回值内联在 <script> 里。平均来说,网络传输的总数据量还是更少的,但要记住 Server Components 不是完全免费的午餐。好在 HTML 会被切成小块流式传输,浏览器不用等全部内容就能开始绘制 UI。
如果你好奇 Server Components 序列化后长什么样,可以看看开发者 Alvar Lagerlöf 做的 RSC Devtools。
🖥️ Server Components 其实不需要服务器
前面我说过,服务端渲染是个"统称",涵盖了好几种策略:
- 静态生成:HTML 在构建时生成,部署的就是静态文件。
- 动态渲染:HTML 在用户请求时"现做现卖"。
RSC 跟这两种策略都能配合。 Server Components 在 Node.js 里渲染时,会生成 JavaScript 对象。这可以在构建时做,也可以在请求时做。
这意味着 不一定需要一个跑着的服务器就能用 RSC!我们可以在构建时把所有静态 HTML 都生成好,然后随便扔到哪个托管服务上。事实上,Next.js App Router 默认就是这么干的。除非你真的需要"动态"渲染,否则所有活儿都在构建时就干完了。
能完全不要 React 吗?
你可能在想:如果一个 Client Component 都没有,还需要下载 React 吗?能用 RSC 做一个完全无 JS 的静态网站吗?
问题是,RSC 目前只能在 Next.js 里用,而 Next.js 本身有一堆需要在客户端跑的代码,比如路由管理之类的。
不过反直觉的是,这反而往往能带来更好的用户体验——比如 Next 的路由器处理链接点击比普通
<a>标签更快,因为它不用加载整个新页面。一个结构合理的 Next.js 应用,在 JS 下载完成前就能用,下载完之后会更快更好。
🚀 RSC 的真正价值
React Server Components 是第一个"官方认证"的在 React 里写服务器专属代码的方式。不过正如前面说的,这在 React 生态里不算新鲜事——2016 年起我们就能在 Next.js 里这么干了!
真正的突破是:以前从来没有办法在组件内部写服务器专属代码。
最明显的好处是性能。Server Components 不会被打包进 JS,需要下载的代码少了,需要水合的组件也少了:
不过坦白说,这反而是我最不兴奋的点。大多数 Next.js 应用在"页面可交互时间"上本来就够快了。
如果你用好语义化 HTML,应用的大部分功能在 React 水合之前就能用了——链接能点,表单能提交,折叠面板能展开(用 <details> 和 <summary>)。对大多数项目来说,水合慢几秒没什么大不了的。
但接下来这点,我觉得真的很酷:我们终于不用在功能和包大小之间做艰难取舍了!
比如说,技术博客一般都需要代码高亮库。我的博客用的是 Prism,代码片段长这样:
function exampleJavaScriptFunction(param) {
return "Hello world!"
}
一个功能完整的代码高亮库,支持所有主流语言,轻轻松松好几兆——塞进 JS 包里太重了。所以我们只能妥协,砍掉不常用的语言和功能。
但如果代码高亮放在 Server Component 里呢? 库的代码压根不会进入 JS 包。我们就不用做任何妥协,想用什么功能就用什么功能。
这就是 Bright 的设计理念——一个专门为 RSC 打造的现代代码高亮库。
这才是让我真正兴奋的地方。 以前因为太重而不敢用的东西,现在可以"免费"在服务器上跑了——给客户端增加零字节,用户体验反而更好。
而且不只是性能和体验。用了一阵 RSC 之后,我越来越欣赏 Server Components 的轻松惬意。再也不用操心依赖数组、过期闭包、记忆化,或者那些因为"值会变"带来的各种头疼问题。
当然,现在还是非常早期的阶段。RSC 几个月前才刚脱离 beta!我很期待看看未来几年社区会搞出什么创新,就像 Bright 这样充分利用新范式的工具。作为 React 开发者,现在真是个激动人心的时代!
🔔 画外音
作者提到的代码高亮库的例子真的很有说服力。想象一下,你可以使用功能最全面的语法高亮库,支持所有编程语言和主题,而这一切对客户端来说都是"零成本"的。这就是 RSC 带来的可能性——把重型计算推到服务器,让客户端保持轻量。
🧩 完整的图景
RSC 很令人兴奋,但它其实只是"现代 React"拼图的一块。
当我们把 RSC 跟 Suspense 和新的 Streaming SSR 架构组合起来,事情就变得真正精彩了。我们能做到一些疯狂的事情:
这超出了本文的范围,但你可以在 GitHub 上了解更多。
这也是我在新课程 The Joy of React 里深入讲解的内容。如果你感兴趣,我很乐意多聊聊!❤️
The Joy of React 是一门对新手友好的互动课程,旨在帮你建立对 React 工作原理的直觉。我们从零基础开始(不需要任何 React 经验),一步步深入到 React 最棘手的那些知识点。
这门课程是我近两年的心血,浓缩了我 10 多年 React 经验里最重要的精华。
课程里有很多好东西。除了 React 本身和这篇博文提到的前沿技术,你还会学到我最喜欢的 React 生态工具。比如,用 Framer Motion 做高级布局动画:
了解更多课程信息:The Joy of React
🎉 总结
React Server Components 是一次重大的颠覆性变革。我个人非常期待未来几年的发展,期待看到生态圈涌现更多像 Bright 这样充分利用 Server Components 的工具。
我有种预感,用 React 做东西马上要变得更酷了。😄
🔔 画外音
读完这篇文章,你应该对 React Server Components 有了一个相当完整的理解。让我们快速回顾一下核心要点:
- RSC ≠ SSR:它们是两个不同的概念,可以结合使用
- 默认是 Server Component:只有需要交互的组件才需要标记为 Client Component
- 客户端边界很重要:理解
'use client'如何创建边界,以及如何通过 children 模式来保留 Server Components- 不是完全免费的:虽然 JS 包变小了,但 HTML 会变大
- 真正的价值:可以使用那些之前因为太大而无法使用的库,而不用担心包大小
希望这篇翻译对你有帮助!如果有任何问题,欢迎在评论区讨论。💖