React Server Components完全指南:服务端渲染的革命性进化
React Server Components完全指南:服务端渲染的革命性进化
文章概述
React在去年庆祝了它的10周年生日!在React首次被引入困惑的开发者社区的十年中,它经历了几次演变。React团队在激进变革方面从不羞涩:如果他们发现解决问题的更好方案,就会去实施。
之前,React团队推出了React Server Components,这是最新的范式转变。这是React组件首次可以完全在服务器上运行。
你将学到:
- React Server Components的核心概念和工作原理
- 与传统SSR的区别和联系
- 客户端组件和服务端组件的边界管理
- 实际应用场景和性能优势
网上对此有很多困惑。许多人对这是什么、如何工作、有什么好处,以及如何与服务端渲染等技术配合使用有很多疑问。
服务端渲染快速入门
为了理解React Server Components的背景,了解服务端渲染(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包添加更多千字节,延长用户必须坐等的时间。
SSR的工作机制
服务端渲染旨在改善这种体验。服务器不发送空HTML文件,而是渲染我们的应用程序生成实际HTML。用户收到完整的HTML文档。
该HTML文件仍包含<script>标签,因为我们仍需要React在客户端运行来处理交互。但我们配置React在浏览器中工作方式略有不同:它不从头创建所有DOM节点,而是接管现有HTML。这个过程被称为水合(hydration)。
React核心团队成员Dan Abramov这样解释:
水合就像用交互性和事件处理程序的"水"浇灌"干燥"的HTML。
一旦JS包被下载,React会快速运行我们的整个应用程序,构建UI的虚拟草图,并将其"匹配"到真实DOM,附加事件处理程序,触发任何effects等。
SSR的核心流程:
- 用户访问网站
- Node.js服务器接收请求,立即渲染React应用程序,生成HTML
- 新鲜出炉的HTML发送给客户端
- 客户端React接管DOM并添加交互性
服务端渲染的变体
"服务端渲染"是一个涵盖多种渲染策略的术语:
静态生成(SSG):在构建应用程序时生成HTML,在部署过程中完成。
动态渲染:用户请求页面时"按需"生成HTML。
我认为"服务端渲染"是一个涵盖多种渲染策略的术语。它们都有一个共同点:初始渲染在Node.js等服务器运行时中进行,使用ReactDOMServer API。何时发生并不重要,无论是按需还是编译时。无论如何,都是服务端渲染。
传统数据获取的问题
让我们谈谈React中的数据获取。通常,我们有两个通过网络通信的独立应用程序:
- 客户端React应用
- 服务端REST API
使用React Query、SWR或Apollo等工具,客户端会向后端发出网络请求,后端然后从数据库获取数据并通过网络发送回来。
客户端渲染的数据流
用户请求 → 空HTML → 下载JS → 渲染外壳 → 请求数据 → 渲染内容
这种模式的问题是用户在看到实际内容之前要经历多个加载阶段。首先看到空白页面,然后是加载状态,最后才是真正的内容。
SSR改进的数据流
即使使用服务端渲染,传统模式仍然是:
服务器渲染外壳 → 客户端水合 → 请求数据 → 渲染内容
这确实是改进——外壳比空白页面好——但实际上并没有显著改善用户体验。用户访问应用不是为了看加载屏幕,而是为了看内容。
理想的数据流
为什么不在初始请求期间进行数据库查询,直接向用户发送完全填充的UI?
数据库查询 → 渲染完整应用 → 发送给客户端
但这需要能够给React一块代码,让它专门在服务器上运行来进行数据库查询。这在React中一直不是选项——即使使用服务端渲染,我们所有的组件都在服务器和客户端上渲染。
React Server Components简介
在高层次上,React Server Components是全新范式的名称。在这个新世界中,我们可以创建专门在服务器上运行的组件。这允许我们做诸如直接在React组件内编写数据库查询之类的事情!
以下是"服务器组件"的快速示例:
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多年的人,这段代码起初对我来说看起来绝对疯狂。
"等等!"我的直觉尖叫道。“函数组件不能是异步的!我们不允许像那样直接在渲染中产生副作用!”
关键理解
关键要理解的是:服务器组件从不重新渲染。它们在服务器上运行一次生成UI。渲染值发送给客户端并锁定。就React而言,这个输出是不可变的,永不改变。
这意味着React API的大部分与服务器组件不兼容:
- 不能使用state,因为state可以改变,但服务器组件不能重新渲染
- 不能使用effects,因为effects只在渲染后在客户端运行,而服务器组件从不到达客户端
这也意味着我们在规则方面有更多灵活性。例如,在传统React中,我们需要将副作用放在useEffect回调或事件处理程序中,这样它们不会在每次渲染时重复。但如果组件只运行一次,我们就不用担心这个!
术语澄清
在这个新范式中,我们熟悉的"传统"React组件被称为客户端组件。老实说,我不喜欢这个名字。
"客户端组件"这个名称暗示这些组件只在客户端渲染,但实际上并非如此。客户端组件在客户端和服务器上都渲染。
术语总结:
- React Server Components:这个新范式的名称
- 客户端组件:我们知道和喜爱的"标准"React组件的新名称。这是旧事物的新名称
- 服务器组件:新类型的组件,专门在服务器上渲染。它们的代码不包含在JS包中,因此从不水合或重新渲染
与SSR的关系
React Server Components不是服务端渲染的替代品。你不应该将React Server Components视为"SSR 2.0版"。
相反,我喜欢将其视为两个完美结合的独立拼图块,两种相互补充的特性。
我们仍然依赖服务端渲染生成初始HTML。React Server Components在此基础上构建,允许我们从客户端JavaScript包中省略某些组件,确保它们只在服务器上运行。
实际上,甚至可以在没有服务端渲染的情况下使用React Server Components,尽管在实践中,两者一起使用会获得更好的结果。
兼容环境与使用方式
通常,当新的React功能出现时,我们可以通过将React依赖项升级到最新版本来在现有项目中开始使用它。快速npm install react@latest就可以开始了。
不幸的是,React Server Components不是这样工作的。
我的理解是React Server Components需要与React之外的许多东西紧密集成,比如bundler、服务器和路由器。
目前,只有一种方式开始使用React Server Components,那就是使用Next.js 13.4+,使用他们全新重新架构的"App Router"。
希望将来,更多基于React的框架将开始整合React Server Components。核心React功能只在一个特定工具中可用感觉很尴尬!
指定客户端组件
在这个新的"React Server 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发出信号的方式,表明该文件中的组件是客户端组件,应该包含在JS包中以便它们可以在客户端重新渲染。
这可能看起来是创建组件类型的一种非常奇怪的方式,但这种事情是有先例的:JavaScript中的"use strict"指令选择进入"严格模式"。
组件选择策略
一般规则:如果组件可以是服务器组件,就应该是服务器组件。
服务器组件往往更简单、更容易推理。还有性能优势:因为服务器组件不在客户端运行,它们的代码不包含在JavaScript包中。
但我们也不应该将消除尽可能多的客户端组件作为使命!不应该试图优化最少数量的客户端组件。值得记住的是,直到现在,每个React应用中的每个React组件都是客户端组件。
需要客户端组件的场景:
- 使用state或effects
- 需要事件处理程序
- 使用浏览器特定API
- 需要实时更新的组件
边界概念:理解组件树的分割
我对React Server Components最初的问题之一是:当props改变时会发生什么?
例如,假设我们有这样的服务器组件:
function HitCounter({ hits }) {
return (
<div>
Number of hits: {hits}
</div>
);
}
如果hits的值从0变成1会怎样?HitCounter需要重新渲染,但它不能重新渲染,因为它是服务器组件!
客户端边界规则
服务器组件实际上不能孤立地理解。我们必须缩小,采用更全面的视图,考虑应用程序的结构。
关键规则:客户端组件只能导入其他客户端组件。
当我们在组件上添加'use client'指令时,我们创建一个"客户端边界"。这个边界内的所有组件都隐式转换为客户端组件。即使像HitCounter这样的组件没有'use client'指令,在这种特定情况下它们仍会在客户端水合/渲染。
// 组件树示例
App (Server Component)
├── Header (Server Component)
├── Article (Client Component - 使用state)
│ ├── HitCounter (隐式Client Component)
│ └── Discussion (隐式Client Component)
└── Footer (Server Component)
这意味着我们不必为每个需要在客户端运行的文件添加'use client'。实际上,我们只需要在创建新客户端边界时添加它。
解决边界限制的方法
当我第一次了解到客户端组件不能渲染服务器组件时,这对我来说感觉相当限制。如果我需要在应用程序高层使用state呢?这是否意味着一切都需要成为客户端组件?
在许多情况下,我们可以通过重构应用程序来解决这个限制,使所有者发生变化。
重构前:
'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>
);
}
重构后:
// 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.js
import Header from './Header';
import MainContent from './MainContent';
import ColorProvider from './ColorProvider';
function Homepage() {
return (
<ColorProvider>
<Header />
<MainContent />
</ColorProvider>
);
}
通过这种重构,Header和MainContent不再隐式转换为客户端组件!
核心理解: 当涉及客户端边界时,父/子关系并不重要。重要的是谁在导入和渲染组件。Homepage决定Header和MainContent的props,由于Homepage是服务器组件,就没有问题。
底层实现:窥探内部机制
让我们在稍低的层面看看这个。当我们使用服务器组件时,输出是什么样的?实际生成了什么?
从超简单的React应用程序开始:
function Homepage() {
return (
<p>
Hello world!
</p>
);
}
当我们在浏览器中访问这个应用时,我们会收到看起来像这样的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>
关键部分解析
-
HTML内容:我们的React应用程序生成的UI,"Hello world!"段落。这要归功于服务端渲染。
-
JS包:加载我们的JS包的
<script>标签。这个包包括React等依赖项,以及应用程序中使用的任何客户端组件。由于Homepage组件是服务器组件,该组件的代码不包含在此包中。 -
内联JS:包含一些内联JS的第二个
<script>标签——这是真正有趣的部分。
self.__next['$Homepage-1'] = {
type: 'p',
props: null,
children: "Hello world!",
};
我们实际上是在告诉React:“嘿,我知道你缺少Homepage组件代码,但别担心:这是它渲染的结果”。
通常,当React在客户端水合时,它会快速渲染所有组件,构建应用程序的虚拟表示。它不能为服务器组件这样做,因为代码不包含在JS包中。
所以,我们发送渲染值,服务器生成的虚拟表示。当React在客户端加载时,它重用那个描述而不是重新生成它。
权衡考虑
这确实意味着虽然我们的JS包变小了,HTML文件变大了。我们不是在JS文件中有组件定义,而是在<script>标签中内联组件的返回值。
平均而言,我们仍会通过网络发送更少的总数据,但值得记住服务器组件不是完全免费的。HTML文件被分块并流式传输,所以浏览器仍然可以快速绘制UI,无需等待<script>标签中的所有内容。
不需要服务器的服务器组件
React Server Components与静态或动态渲染策略兼容。当服务器组件在Node.js运行时中渲染时,它们返回的JavaScript对象将被创建。这可以按需发生或在构建期间发生。
这意味着可以在没有服务器的情况下使用React Server Components!我们可以生成一堆静态HTML文件并在任何地方托管它们。实际上,这是Next.js App Router中默认发生的。除非我们真的需要"按需"发生事情,否则所有这些工作都会在构建期间提前进行。
React Server Components的优势
React Server Components是在React中运行服务器专用代码的第一种"官方"方式。如我之前提到的,这在更广泛的React生态系统中并不是真正的新事物;自2016年以来我们就能够在Next.js中运行服务器专用代码!
大区别是我们以前从未有办法在组件内部运行服务器专用代码。
性能优势
最明显的好处是性能。服务器组件不包含在JS包中,这减少了需要下载的JavaScript数量,以及需要水合的组件数量。
性能指标对比:
| 指标 | 传统方式 | RSC方式 | 改善 |
|---|---|---|---|
| JS Bundle Size | 大 | 小 | 显著减少 |
| 水合时间 | 长 | 短 | 明显缩短 |
| 首次内容绘制 | 延迟 | 提前 | 用户体验提升 |
| 交互时间 | 延迟 | 提前 | 响应性提升 |
功能优势
更令我兴奋的是:我们不再需要在功能与包大小之间做出同样的妥协!
实际案例:语法高亮
大多数技术博客需要某种语法高亮库。合适的语法高亮库,支持所有流行编程语言,将是几兆字节,太大而无法放入JS包。因此,我们必须做出妥协,削减不是关键任务的语言和功能。
但假设我们在服务器组件中进行语法高亮。在这种情况下,库代码实际上都不会包含在JS包中。因此,我们不必做任何妥协,可以使用所有功能。
// 服务器组件中的语法高亮
import { highlight } from 'comprehensive-syntax-highlighter';
function CodeBlock({ code, language }) {
const highlightedCode = highlight(code, language);
return (
<pre>
<code dangerouslySetInnerHTML={{ __html: highlightedCode }} />
</pre>
);
}
这就是让我对React Server Components感到兴奋的事情。在JS包中成本过高而无法包含的东西现在可以在服务器上免费运行,为我们的包添加零千字节,并产生更好的用户体验。
开发体验优势
这不仅仅关于性能和用户体验。使用RSC一段时间后,我开始真正欣赏服务器组件是多么轻松。我们永远不必担心依赖数组、过时闭包、记忆化,或任何由事物变化引起的其他复杂东西。
服务器组件的简单性:
- 无状态管理复杂性
- 无生命周期方法
- 无记忆化需求
- 直接的数据获取
- 简化的错误处理
实际应用场景
数据获取组件
// 服务器组件:直接数据库查询
import { db } from '@/lib/database';
async function ProductList({ category }) {
const products = await db.product.findMany({
where: { category },
orderBy: { createdAt: 'desc' },
take: 10
});
return (
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
内容生成组件
// 服务器组件:Markdown渲染
import { remark } from 'remark';
import html from 'remark-html';
async function BlogPost({ slug }) {
const post = await getPostBySlug(slug);
const processedContent = await remark()
.use(html)
.process(post.content);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{
__html: processedContent.toString()
}} />
</article>
);
}
第三方API集成
// 服务器组件:天气数据
async function WeatherWidget({ city }) {
const weather = await fetch(
`https://api.weather.com/v1/current?key=${process.env.WEATHER_API_KEY}&q=${city}`
).then(res => res.json());
return (
<div className="weather-card">
<h3>{city}天气</h3>
<p>温度: {weather.temperature}°C</p>
<p>状况: {weather.condition}</p>
</div>
);
}
最佳实践与设计模式
组件架构设计
推荐的组件分层:
App Layout (Server Component)
├── Header (Server Component)
├── Navigation (Client Component - 需要交互)
├── Main Content (Server Component)
│ ├── Article List (Server Component - 数据获取)
│ └── Interactive Features (Client Component)
└── Footer (Server Component)
数据获取策略
// 推荐:组件级数据获取
async function UserProfile({ userId }) {
const user = await fetchUser(userId);
const posts = await fetchUserPosts(userId);
return (
<div>
<UserInfo user={user} />
<PostList posts={posts} />
</div>
);
}
// 避免:在客户端组件中数据获取
'use client';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// 需要处理加载状态...
}
错误处理
// 服务器组件错误边界
async function ProductPage({ productId }) {
try {
const product = await fetchProduct(productId);
if (!product) {
return <ProductNotFound />;
}
return <ProductDetail product={product} />;
} catch (error) {
console.error('Failed to fetch product:', error);
return <ProductError error={error} />;
}
}
性能优化策略
减少客户端JavaScript
// 优化前:所有组件都是客户端组件
'use client';
function Dashboard() {
const [user] = useState(getCurrentUser());
return (
<div>
<Header user={user} />
<Sidebar navigation={NAVIGATION} />
<MainContent data={user.data} />
<Footer />
</div>
);
}
// 优化后:最小化客户端边界
function Dashboard() {
return (
<div>
<Header /> {/* Server Component */}
<Sidebar /> {/* Server Component */}
<UserContextProvider> {/* Client Component */}
<MainContent /> {/* 在客户端边界内 */}
</UserContextProvider>
<Footer /> {/* Server Component */}
</div>
);
}
智能缓存策略
// 服务器组件缓存
import { cache } from 'react';
const getCachedUser = cache(async (userId) => {
return await db.user.findUnique({
where: { id: userId }
});
});
async function UserProfile({ userId }) {
const user = await getCachedUser(userId);
return (
<div>
<h1>{user.name}</h1>
<UserStats userId={userId} /> {/* 会重用缓存的用户数据 */}
</div>
);
}
流式渲染优化
// 使用Suspense实现流式渲染
import { Suspense } from 'react';
function ProductPage({ productId }) {
return (
<div>
<ProductHeader productId={productId} />
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={productId} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<ProductRecommendations productId={productId} />
</Suspense>
</div>
);
}
调试与开发工具
组件类型识别
// 开发环境中标识组件类型
function ComponentTypeIndicator({ children, type }) {
if (process.env.NODE_ENV === 'development') {
return (
<div data-component-type={type}>
{children}
</div>
);
}
return children;
}
// 使用
function MyServerComponent() {
return (
<ComponentTypeIndicator type="server">
<div>Server Component Content</div>
</ComponentTypeIndicator>
);
}
性能监控
// 服务器组件性能追踪
async function DataHeavyComponent() {
const startTime = performance.now();
try {
const data = await expensiveDataOperation();
if (process.env.NODE_ENV === 'development') {
const endTime = performance.now();
console.log(`Data fetch took ${endTime - startTime}ms`);
}
return <DataDisplay data={data} />;
} catch (error) {
console.error('Data fetch failed:', error);
throw error;
}
}
未来展望:完整的现代React生态
React Server Components只是"现代React"拼图的一部分。
当我们将React Server Components与Suspense和新的流式SSR架构结合时,事情变得真正有趣。它允许我们做这样的事情:
// 未来的React应用架构
function App() {
return (
<html>
<body>
<Header /> {/* 立即渲染 */}
<Suspense fallback={<MainSkeleton />}>
<MainContent /> {/* 流式渲染 */}
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar /> {/* 并行渲染 */}
</Suspense>
<Footer /> {/* 立即渲染 */}
</body>
</html>
);
}
这种架构允许:
- 外壳立即渲染和发送
- 数据获取并行进行
- 内容流式传输到客户端
- 渐进式水合
- 最优的用户体验
生态系统发展
新兴工具和库:
- Bright - 为RSC设计的语法高亮包
- RSC DevTools - 开发和调试工具
- 服务器组件UI库 - 专为服务器组件设计的组件库
- 数据获取库 - 原生支持RSC的数据层
框架支持情况
| 框架 | RSC支持 | 状态 | 特性 |
|---|---|---|---|
| Next.js 13+ | ✅ | 稳定 | App Router, 完整支持 |
| Remix | 🔄 | 开发中 | 实验性支持 |
| Gatsby | 📋 | 计划中 | 路线图中 |
| Create React App | ❌ | 无计划 | 社区方案 |
常见问题与解决方案
迁移策略
渐进式迁移步骤:
-
评估现有代码库
- 识别纯展示组件
- 找出数据获取逻辑
- 标记交互性组件
-
创建迁移计划
- 优先迁移叶子组件
- 逐步向上移动
- 保持功能稳定
-
执行迁移
- 小步迭代
- 充分测试
- 性能监控
常见陷阱
1. 边界混乱
// 错误:尝试在服务器组件中使用客户端特性
function ServerComponent() {
const [state, setState] = useState(0); // ❌ 错误
useEffect(() => {
// ❌ 错误
}, []);
return <div onClick={() => {}}> {/* ❌ 错误 */}</div>;
}
// 正确:明确组件类型和职责
'use client';
function ClientComponent() {
const [state, setState] = useState(0); // ✅ 正确
return <div onClick={() => setState(s => s + 1)}>{state}</div>;
}
2. 数据重复获取
// 错误:重复获取相同数据
async function ParentComponent() {
const user = await fetchUser();
return (
<div>
<UserProfile userId={user.id} /> {/* 会再次获取用户 */}
<UserPosts userId={user.id} />
</div>
);
}
// 正确:传递已获取的数据
async function ParentComponent() {
const user = await fetchUser();
return (
<div>
<UserProfile user={user} />
<UserPosts userId={user.id} />
</div>
);
}
总结与最佳实践
React Server Components代表了React开发的重大范式转变。它不是简单的技术升级,而是思维方式的改变——从客户端为中心转向服务端和客户端协作。
核心收获:
- 理解边界概念 - 服务器组件和客户端组件的分界线是关键
- 明智的组件选择 - 能用服务器组件就用服务器组件
- 数据获取革命 - 直接在组件中获取数据,无需复杂的状态管理
- 性能优化新机会 - 减少JavaScript包大小,提升用户体验
- 开发体验改善 - 更简单的心智模型,更少的复杂性
实践指南:
- 从小项目开始实践RSC概念
- 逐步迁移现有项目,不要急于求成
- 充分利用服务器组件的优势(数据获取、内容生成)
- 保持客户端组件的最小化但不过度优化
- 关注用户体验而非技术炫技
未来趋势:
- 更多框架将支持RSC
- 生态系统工具不断完善
- 开发模式进一步标准化
- 性能优化技术持续发展
更多推荐

所有评论(0)