
RSC 的核心不是"更快",而是把渲染和数据获取搬回服务端。真正决定项目成败的是 'use client' 划在哪里——划错一处,整棵子树都会退回客户端。
先说结论:Server Components 带来的不是"性能开关",而是一次职责重划:默认在服务端渲染,只在需要交互的最小范围标
'use client'。最容易踩的坑也正是这个指令——它在模块图上是一道"污染边界",一旦标在靠近根组件的位置,整棵子树连同其依赖都会被打包进客户端。所以实战中真正要练的不是写 RSC,而是判断边界该切在哪。
很多人以为 'use client' 是"这个组件在客户端渲染"。不准确。它的真实语义是:这个模块及其导入的所有模块,属于客户端包。
这意味着两个后果:
所以一个常见的性能事故是:为了给页面加一个下拉菜单,把 'use client' 标在了布局组件上,结果整页的依赖全部进了客户端 bundle。体积涨了,但没人知道为什么。
原则只有一条:交互标记尽量下沉到真正需要状态的最小单元。
举个具体的例子。一个商品详情页需要:展示商品信息(静态)、切换规格(交互)、加入购物车(交互)、展示评价列表(静态)。
错误写法是把整个页面标成 client 组件,然后在里面 fetch 数据。这样服务端渲染的好处几乎全丢了。
正确写法是页面本身保持 Server Component,只在规格选择器和加购按钮上标 client:
// app/product/[id]/page.js —— Server Component,不标 'use client'
export default async function Page({ params }) {
const product = await getProduct(params.id); // 直接在服务端查库
return (
<main>
<ProductInfo data={product} /> {/* 静态,服务端渲染 */}
<SkuPicker skus={product.skus} /> {/* 交互,client */}
<ReviewList productId={product.id} /> {/* 静态 */}
</main>
);
}
这样只有 SkuPicker 及其依赖进客户端包,商品数据和评价列表都在服务端就渲染成 HTML 了。
RSC 里可以直接写 await,不需要 useEffect + loading 状态那一套。这不是语法糖,而是消除了"先渲染空壳、再请求数据、再重渲染"的三段式闪烁。
实践中的几条经验:
Promise.all,不要串行 awaitServer Component 传给 Client Component 的 props 必须可序列化。这意味着不能传函数、不能传 class 实例、不能传 Date 对象的某些用法(不同版本行为有差异,建议统一转成字符串)。
想把"点击后做什么"传给客户端组件,正确的做法是传数据 + 用 Server Actions,而不是传回调函数。这是从 React 传统写法迁移过来时最容易出错的地方。
Next 14 里 fetch 默认会被缓存,这个默认值踩过很多人。三档控制:
| 写法 | 含义 | 适用 |
|---|---|---|
cache: 'force-cache' | 永久缓存,除非手动 revalidate | 极少变动的配置、字典 |
next: { revalidate: 60 } | 60 秒内用缓存 | 列表页、文章页 |
cache: 'no-store' | 每次都重新请求 | 用户中心、订单、库存 |
还有一个容易忽略的点:revalidatePath 要在数据变更之后调用,而且路径要写对。写错了不会报错,只是页面上永远是旧数据,排查起来很费时间。
RSC 不是万能方案。以下几种情况用传统方案更省事:
'use client''use client' 都在叶子组件上,且能用 grep 数出来有几个最后一条最实用:与其在架构图上争论边界,不如直接看构建产物的体积变化。数字会告诉你边界划错在哪。

添加微信客服,1 对 1 沟通