news 2026/9/19 19:46:05

React Server Components 解决什么问题?从 CSR 到 Next.js 组件体系全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Server Components 解决什么问题?从 CSR 到 Next.js 组件体系全解析

React Server Components 到底解决什么问题?搞懂 CSR 与 Next.js 组件体系,前端架构才算入门

先说结论:React Server Components 是这几年 React 生态里最值得重新理解的概念之一,它直接改变了我们写前端时对“组件运行在哪里”的认知。以前我们默认组件都在浏览器里跑,现在有了服务端组件之后,一部分组件可以在服务器上执行,渲染成 HTML 或特殊序列化格式再发给客户端,JS 包体积大幅下降,数据获取也变得更直接。这个思路在 Next.js 的 App Router 里已经全面落地,也是当前 React 面试里被问烂的高频题。我最初接触时也有大量困惑,比如它和 SSR 到底什么关系?为什么有了 SSR 还要 Server Components?组件文件里加一行 "use client" 和以前写客户端组件有什么区别?这些如果不从原理层面捋清楚,后面写代码很容易踩坑。

这篇文章我会从最基础的 CSR 出发,一步步讲清 React Server Components 的设计动机、工作原理,以及在实际工程里怎么区分和使用客户端组件、服务端组件。无论你是刚开始学 React 的新手,还是用 Next.js 做过几个项目的开发者,这都能帮你把渲染模式的底层逻辑串起来,搭建起一个清晰的架构认知。


1. 先说清楚我们到底在解决什么问题:CSR 的瓶颈在哪里

1.1 浏览器渲染的起点:什么是 CSR

CSR,全称 Client Side Rendering,中文叫客户端渲染。这是 React 自诞生以来最经典的渲染方式。它的流程是:服务器先返回一个几乎为空的 HTML 文件,里面挂着一个<div id="root"></div>,然后浏览器下载一堆 JS 文件,执行这些 JS,在内存里构建出虚拟 DOM,再渲染成真实 DOM 放到页面上。

整个过程听起来很顺畅,但问题也藏在里面:用户在浏览器看到内容之前,必须等待 JS 包下载、解析、执行完毕。网络差、设备旧、包体积大的时候,这个白屏时间会非常显眼。你可以把这种模式理解成一家餐厅把整套厨房设备都搬到顾客面前,顾客看完菜单后厨师才能在你面前开始做菜。做菜过程就是 JS 执行的瞬间,而菜单就是 HTML。

React 刚火的那几年,这种模式并没有显得那么糟糕,因为应用复杂度有限,JS 包体量也还压得住。直到后来中后台应用动辄几百 KB 甚至数 MB 的 JS 体积出现,CSR 的性能问题变成了无法回避的痛点,才催生了后面一系列的渲染优化方案。

1.2 CSR 的核心痛点:白屏、SEO 与双倍工作

CSR 的痛点可以拆成三个层面来看。

第一个是白屏时长。用户访问首屏,浏览器要先下载 HTML,然后下载 JS,再执行 JS,最后结合数据渲染出内容。这个过程里任何一步慢了,用户看到的就是白屏或骨架屏。尤其在国内复杂的网络环境下,第三方 CDN 不稳定、公共库加载失败等都会让体验雪上加霜。

第二个是SEO 不友好。搜索引擎爬虫虽然这些年解析 JS 的能力越来越强,但对海量内容型网站来说,服务端直接返回可读的内容仍然是更稳妥的做法。如果你的页面完全依赖客户端渲染,爬虫拿到的是一个空壳,收录效果会打折。

第三个是开发层面的“双倍工作”感。同一个应用里,业务逻辑写在 React 组件里,但服务端可能还要单独维护一套渲染代码或接口层,典型情况是:前端用 React 写页面,后端用 Java/Python/Node 再写一套模板引擎页面,逻辑重复、维护成本高。CSR 本身没问题,但当你需要快速实现一个兼具首屏速度和内容可访问性的站点时,只靠 CSR 是远远不够的。

1.3 传统方案盘点:SSR、SSG、ISR 做了什么,又留下了什么

为了解决 CSR 的白屏和 SEO 问题,圈子里陆续出现了 SSR(服务端渲染)、SSG(静态站点生成)、ISR(增量静态再生)三种方案。

SSR 的思路是:每次请求进来,服务器直接执行组件代码,拼接出完整 HTML 返回给浏览器。用户在 HTML 到达时就能看到内容,JS 包后续再加载用来做交互。这种方式解决了首屏白屏和 SEO,但也带来新的问题:服务端压力大、每次请求都要动态渲染,而且你仍然要把一大份 React 代码发送到客户端,用于“水合”(Hydration),让页面上的事件能响应起来。

SSG 更进一步,在构建阶段就把页面渲染成静态 HTML,部署到 CDN 上,访问速度极快。但问题是:适合静态内容的场景有限,内容一变就得重新构建。

ISR 算是 SSG 的升级版,允许你在构建后按需重新生成部分页面,但仍有不少配置成本和心智负担。

你会发现这几种方案本质上都是在“渲染发生的位置和时间”上做文章:CSR 在浏览器运行,SSR 在请求时运行,SSG 在构建时运行。它们解决了首屏渲染问题,但有一个核心问题始终没被碰:组件本身还是统一的 React 组件,不管在哪渲染,最终都要把 JS 塞给浏览器。这种“全有或全无”的包袱,直到 React Server Components 出现才被真正打破。


2. React Server Components 的核心概念:它和你以为的不一样

2.1 一句话讲清 RSC 是什么

React Server Components 是 React 官方提出的一种组件模型,它不是某个库,也不是 Next.js 特有的功能,而是 React 底层的能力。它的核心思想是:让一部分组件只在服务器端运行,这些组件的代码不会打包发送到浏览器,但渲染结果可以以序列化数据的形式传递给客户端。

你可以把整个 React 应用想象成一家餐厅,CSR 模式下所有食材和厨师都在顾客面前,而 Server Components 则是把后厨搬到了远方,只把菜品端给顾客。顾客吃到的菜(渲染结果)完全一样,但需要展示给顾客看的厨房设备和人员(JS 代码)大幅减少了。

这个模型带来的直接好处有两个。第一,首屏加载的 JS 体积大幅减小,因为服务端组件的代码根本不会发到客户端。第二,数据获取链路变简单,服务端组件可以直接读取数据库、调用内部接口,不需要额外封装 API 层。

2.2 Server Components 不等于 SSR,别再搞混了

我在和很多同行交流时发现,最常被混淆的就是 Server Components 和 SSR。两者虽然都在服务端渲染,但定位完全不同。

SSR 的核心是解决“首屏 HTML 能不能快点让用户看到”,它是一种渲染策略。渲染完之后,React 代码仍然要完整下发到浏览器,水合之后页面进入可交互状态。也就是说,SSR 其实是 CSR 的增强版,最终运行的还是客户端组件。

Server Components 则是一种新的组件类型。它的代码只在服务端执行,不会发送到客户端,所以也没有对应 JS 去客户端水合。它可以嵌入其他客户端组件,也可以被客户端组件通过特殊机制引用。

理解这个区别后,你会明白:SSR 和 Server Components 不是替代关系,而是解决不同维度的两个方案。前者关注“页面何时渲染成 HTML”,后者关注“组件在哪个环境运行、代码要不要发给浏览器”。在 Next.js App Router 里,它们其实是配合使用的:页面默认就是服务端组件,并且可以流式渲染到客户端,形成一套新的渲染范式。

2.3 Server Components 的序列化协议:它是怎么传数据的

Server Components 渲染完成后,生成的不是普通 HTML,而是一种特殊的序列化格式,官方称为 Flight 格式。这里用 Flight 代指 React Server Components 的通信协议,它可以把服务端组件的渲染结果编码成一棵组件树,然后传输给客户端。

Flight 格式的传输内容包括三类信息:

  • 该渲染哪些组件(组件引用)
  • 每个组件接收什么 props(数据)
  • 哪些部分是客户端组件的占位位置(需要客户端运行时去补全)

正是这种格式,让客户端拿到结果后,能知道哪些位置需要加载哪些客户端组件,哪些内容直接嵌在数据里,不需要额外请求。打个比方,Flight 就像一份装修图纸,服务端通过图纸告诉客户端:这面墙刷白、这里放沙发、这里有块区域你别管,后续我会把沙发组件运过来。

2.4 RSC 的关键特性解读

RSC 有三个关键特性,理解它们才能真正掌握这个技术。

第一个是零客户端开销。被标记为服务端的组件不会被打包进客户端 bundle,所以那些只在服务端使用的库(如加密模块、数据库驱动)完全不会增加浏览器体积。我在实际项目里,曾把一个大表单页面从一个纯客户端组件拆分成服务端获取初始数据和客户端交互组件,打包体积从 320KB 降到了 90KB 左右,首屏速度肉眼可见提升。

第二个是自动代码分割。以前我们习惯用React.lazynext/dynamic做代码分割,RSC 模式下这是自动的。服务端组件天然不会进入客户端 bundle,客户端组件也可以按需加载。你不需要手动拆分,架构层面就完成了优化。

第三个是直接访问后端资源。服务端组件在服务端执行,天然可以读取数据库、调用内部服务。这听起来简单,但实际开发体验改变很大——以前你得先封装 API 接口,前端再通过 fetch 拿数据,现在直接在组件里写异步函数获取数据即可,大大减少了胶水代码。


3. Next.js 中的客户端/服务端组件:搞清楚"use client"和"use server"

3.1 App Router 的组件模型

Next.js 从 13 版本开始引入 App Router,彻底把服务端组件作为默认选项。在 App Router 下,所有组件默认都是 Server Component,只有当你明确标记 "use client" 时,它才变成 Client Component。

这也意味着,你写的每个组件文件,首先要回答一个问题:这个组件是需要在浏览器里跑,还是在服务器上跑?如果不特意声明,它就是 Server Component。你可以直接在服务器组件里await获取数据、直接导入数据库客户端,这种写法在很多初学者看来挺神奇的,但它确实就是 App Router 的设计初衷。

Pages Router 时代和 App Router 时代是两种完全不同的心智模型。Pages Router 下getServerSidePropsgetStaticProps是服务端的专属函数,组件本身永远是客户端组件。App Router 则把边界下沉到了组件级别,API 路由和数据获取都现代化了许多,但也引入了新的学习成本:你不再只是写 React 组件,而是需要明确区分不同组件类型。

3.2 "use client" 到底是什么

很多新手会把 "use client" 当成一个导入语句或者某种魔法注释,其实它是一个指令,用来声明:这个文件中的组件是 Client Component,它需要被打包到浏览器端运行。

但要注意,"use client" 不是把所有东西都拉到客户端。当服务端组件导入一个客户端组件时,这个客户端组件仍然可以在服务端预渲染 HTML,只是它同时也准备了浏览器端 JS 用于水合和交互。换句话说,客户端组件是“可以在服务端渲染,但必须在客户端运行”的组件,两者并不互斥。

还有一点容易被忽略:"use client" 是文件级的指令,不精确到组件粒度。一个文件一旦标记了 "use client",文件里所有的导出都视为客户端组件。所以在实际开发中,如果一个文件里同时有服务端组件和客户端组件的部分,最好拆成多个文件,把边界画清楚。

我在项目里吃过这个亏:把一个工具函数文件顺手加了 "use client",结果后续所有用到这个文件里函数的地方都变成了客户端组件,导致某块本应待在服务端的渲染逻辑被打包到了浏览器。后来花了不少时间排查才发现是文件级指令导致的“边界泄漏”。

3.3 "use server" 与 Server Actions

除了组件标记,Next.js 还提供了 "use server" 指令,主要用于 Server Actions 场景。你可以把它理解成“让客户端直接调用服务端函数”,而不需要手动创建 API 路由。

用法上,在异步函数顶部加 "use server",或者在一个单独文件里把所有导出函数声明为服务端动作。比如:

// app/actions.ts "use server"; export async function updateUser(id: string, data: FormData) { // 在这里直接操作数据库 // 这段代码只在服务端运行 console.log("server action called"); }

然后在客户端组件里导入调用:

import { updateUser } from "./actions"; function UserForm() { return ( <form action={updateUser}> <input name="name" /> <button type="submit">更新</button> </form> ); }

这个能力对表单交互特别有用,因为以前你要在客户端收集数据、调用 API、处理 loading 状态,现在可以直接绑定 action,服务端执行完返回结果,代码量直接少一大截。当然,这种便捷背后也埋了一些坑,后面我会单列一节专门讲。

3.4 组件边界的传播规则

理解组件边界的传播规则很重要,因为它决定了你的应用有哪些部分会进入客户端 bundle。

基本规则是:Server Component 可以导入 Client Component,但 Client Component 导入 Server Component 时,这个 Server Component 会变成某种意义上的“插槽”。什么意思呢?比如你有一个客户端组件Dashboard,它没办法直接导入一个服务端组件StatsChart并使用它,因为它俩运行环境不同。正确的做法是:把一个StatsChart作为 children 或 prop 传进客户端组件,这个 children 由外层服务端组件来填充。

代码示例:

// app/dashboard/page.tsx // 这是一个 Server Component import StatsChart from "./StatsChart.server"; import Dashboard from "./Dashboard.client"; export default function Page() { return ( // StatsChart 在服务端渲染好,作为 children 传给客户端组件 <Dashboard> <StatsChart /> </Dashboard> ); }

这条规则初期容易让人困惑:为什么客户端组件不能直接导入服务端组件?因为如果真的允许,会迫使浏览器去下载服务端组件的运行时依赖,打破“服务端代码不出网”的承诺。

把握住“边界”和“传导”这两个词,App Router 的组件模型基本就清晰了。实际开发中我建议你像画关系图一样,把每个组件的类型标记出来,从页面入口开始往下捋。


4. 实操指南:从零配置一个 React Server Components 项目并写清组件边界

4.1 搭建 Next.js App Router 项目

最简单的上手路径是直接用 Next.js 官方脚手架。不要先想着从零手写 RSC 的编译环境,除非你想深入源码,否则站在 Next.js 的肩膀上是效率最高的方式。

npx create-next-app@latest my-rsc-demo # 选择 TypeScript、TailwindCSS(可选)、App Router cd my-rsc-demo npm run dev

创建完成后,你会发现默认的app/page.tsx就是一个服务端组件。如果要在里面用客户端状态,就得在文件顶部加上"use client",或者在目录下拆出一个.client.tsx文件。官方脚手架已经帮你配置好了所有编译细节,你要做的只是想清楚哪些组件放哪一层。

4.2 区分一个组件的关键步骤

给你一套我平时做组件分类的决策方法,特别适合新手。

第一步,看这个组件需不需要交互。需要useStateuseEffectuseContext,或者要响应浏览器事件,那它必然是客户端组件。

第二步,看它依赖了哪些数据。如果数据来自服务端数据库、内部 API 或后端服务,优先考虑让它留在服务端组件,直接在服务端取数据。

第三步,看它有没有被环境绑定的库。比如用了windowdocumentlocalStorage,就必须是客户端组件。如果用了fs、数据库驱动、私密密钥,就必须是服务端组件。

第四步,把组件拆分成容器和交互两层。我常用的模式是:外层服务端组件负责取数,渲染框架结构;内层客户端组件接收数据,管理交互状态。data 通过 props 传入,边界自然就清晰了。

举个典型例子,一个用户资料展示加编辑的页面:

// app/profile/page.tsx // 服务端组件:取数据、展示框架 import { getUser } from "@/lib/db"; import ProfileEditor from "./ProfileEditor.client"; export default async function ProfilePage() { const user = await getUser(session.userId); return <ProfileEditor user={user} />; }
// app/profile/ProfileEditor.client.tsx // 客户端组件:负责表单交互 "use client"; import { useState } from "react"; export default function ProfileEditor({ user }) { const [name, setName] = useState(user.name); return ( <input value={name} onChange={(e) => setName(e.target.value)} /> ); }

注意ProfilePage在服务端执行,用户数据直接注入到客户端组件的 props 里。客户端组件代码里没有任何数据库逻辑,边界很干净。

4.3 序列化限制:哪些数据不能传

这个坑特别值得单独拿出来讲:Server Component 传给 Client Component 的 props,必须是可以序列化的。

官方支持的数据类型包括基本类型、普通对象、数组、JSON 兼容数据,以及部分 React 内置类型。但以下这些会直接报错或静默失效:

  • Date对象:虽然有时能工作,但本质上会变成字符串,建议传时间戳或 ISO 字符串
  • Map/Set:不能直接传递
  • 类实例:序列化会丢失原型链
  • File/Buffer等二进制对象
  • 函数:绝对不能传函数作为 Serialized Props 从服务端传到客户端

很多人踩过这个坑。函数本身就是“客户端才能执行的逻辑”,如果你试图把一个服务端函数传给客户端组件,React 会在运行时抛出序列化错误,提示你不支持函数类型。这个限制的底层逻辑,还是服务端不向客户端暴露代码这一原则。

我建议的做法是:所有非序列化数据要么在服务端组件里先处理成基础类型,再传给客户端组件;要么通过 Server Actions / API 接口去访问,而不是硬塞到 props 里。

4.4 Server Actions 的使用与坑

Server Actions 确实好使,但我见过不少项目滥用它,最后维护变成灾难。分享几个实用经验:

第一,别把整个业务逻辑塞进一个 action 函数。拆成小函数,方便复用和测试。每个 action 函数尽量职责单一,就像写 REST API 一样。

第二,注重权限校验。action 本身是一种 API 的“马甲”,任何人通过网络发起请求都可能触发它。你在 action 里做数据库操作之前,务必先做身份校验和权限判断,别因为写起来像“直接函数调用”就忽略了安全边界。

第三,form action 的返回值要约定好。可以用useActionState绑定表单状态,统一处理成功和失败提示,避免在每个表单里写大量重复的状态管理逻辑。

第四,Server Actions 触发时会带上一些默认的协议信息(例如加密的 action ID、来源校验等),部署时要注意反向代理和 CDN 配置不能剥掉这些请求头,否则会出现请求失败。

4.5 两种组件混用时的编写规则

在实际文件里,服务端组件和客户端组件混用的频率很高。我整理几条经验规则,可以帮你减少乱七八糟的边界报错:

  • 服务端组件里可以自由导入服务端组件和客户端组件,也可以await数据获取函数。
  • 客户端组件里只能导入其他客户端组件,或者接受服务端组件传来的children/ props。
  • 如果你需要在客户端组件里引用一段“服务端渲染结果”,请用children模式或通过 props 传ReactNode
  • 建议通过文件后缀或者目录来统一标识组件类型。比如.server.tsx.client.tsx,这样团队协作时一眼就能看出来组件类型,减少跨文件误判的概率。
  • 不要轻易把第三方库导出的组件“直接包装”成客户端组件,先观察它依赖的浏览器 API。一些小工具库(如日期处理、格式化库)在服务端也能跑,但真正维护 DOM 或使用 hook 的库,必须要客户端包裹层。

我在项目里习惯建一个components/目录,用两层结构:server/client/,再在根目录写一个简单的格式说明.md,把组件选型流程固定下来。团队新增页面时,按这个流程走,基本不会踩边界雷区。


5. 一张图理清选型逻辑:何时用 Server Components,何时用 Client Components

5.1 选型决策表

下面这张表是我做技术评审时会直接拿出来的对照表。它并不是绝对标准,但能覆盖大多数场景。

需求/场景推荐组件类型思考过程
页面主要用于展示内容,无交互Server Component服务端渲染完发 HTML,零 JS 负担
页面需要状态管理(筛选、分页、弹窗)Client Component状态属于客户端运行时能力
数据来自数据库/内部服务Server Component去掉 API 胶水层,减少请求次数
需要调用浏览器 API(window等)Client Component服务端没有这些 API
表单提交 + 数据更新Server Actions + Client Form服务端函数直接处理,客户端负责展示
首屏性能要求极高的内容站Server Component + Streaming流式输出比大块 HTML 拼接更快
高度动态的仪表盘,数据实时刷新Client Component轮询或 WebSocket 天然适合客户端
需要引入第三方图表库Client Component图表库大量操作 DOM,必须浏览器运行

5.2 Server Components 的优势场景

从实际收益来说,三类场景最适合引入 Server Components。

一是内容型页面。博客、文档、新闻列表、商品详情页。这类页面数据量大、交互少,服务端渲染直接输出内容,页面首屏非常快,SEO 也友好。我在做电商详情页优化时,把商品描述、规格参数、推荐列表全放到服务端组件,移动端首屏时间快了不少,核心原因就是减少了客户端需要加载的 JS。

二是中后台系统的列表页。列表页通常包含大量的查询、分页、过滤逻辑,但这些逻辑大部分发生在服务端。以前我们要写一堆 useEffect 去 fetch 数据、管理 loading 状态、拼接口参数,服务端组件 + 表单/链接方式的分页可以直接让数据在服务端检索,客户端只需要触发导航。代码量减少,数据请求次数也少了。

三是需要数据聚合的页面。比如多系统数据汇总的 dashboard,服务端组件可以同时请求多个内部服务,并行聚合数据再渲染,不用把多个 API 暴露给客户端。

5.3 不要硬上 Server Components 的场景

但是,不要为了用而用。如果是下面这些场景,继续用 Client Components 反而更合适:

  • 一个非常独立的交互组件(比如富文本编辑器、拖拽看板、实时协作白板),它的整个生命周期都在浏览器里。
  • 需要大量客户端状态的页面。比如一个所有内容都依赖筛选条件的图表面板,把数据全拉到客户端再筛选反而更快。
  • 老旧项目迁移。如果项目已经基于 Pages Router + 大量客户端状态开发,迁移到 Server Components 的改造成本会很高,收益也未必明显,建议新页面再考虑。

5.4 性能与体验的关键取舍

工程实践里,我一般把渲染方案的选择看成一次“性能预算”的分配。CSR 时代,所有 JS 都压到客户端,所以你的脑力都花在如何拆分 bundle、如何懒加载。而 Server Components 时代,你要想的是:哪些代码可以在服务端跑?哪些数据可以在服务端取?哪些组件能不进客户端 bundle?

这种思维的转变,比单纯记熟一个技术 API 重要得多。以前我们在浏览器里被迫下载整个应用的 JS 运行时,很多库根本用不上,现在通过组件类型标记,运行时体积和责任范围都能被约束得更好。从这个角度说,RSC 不只是一个性能优化工具,它更像一种架构分层:服务端负责数据与渲染,客户端负责交互与体验,两者通过明确的边界通信,各司其职。


6. 实操中最常见的 6 个问题与排查技巧

6.1 组件边界报错:"You're importing a component that needs useState"

这是 RSC 模式下最常见的报错之一。原因很直接:你在一个 Server Component 里使用了useStateuseEffect等客户端 Hook,React 马上就会提醒你边界出了问题。

排查思路分三步走:

  1. 检查当前文件是否有"use client"指令,如果没有就是 Server Component。
  2. 确认报错 Hook 所在的文件,是不是在层级上被 Server Component 导入。
  3. 把需要用 Hook 的部分拆成一个独立文件,加"use client",然后由外层服务端组件导入它,或者通过 children 传进去。

切记,"use client"是文件级指令,别用一个文件里所有组件混合状态管理的方式逃避拆分。

6.2 序列化报错:props 传了函数或类实例

运行时出现Error: Functions cannot be passed directly to Client Components时,意思是你把不可序列化的东西塞给了客户端组件 props。

解决方式有两种面:

  • 如果是函数,改用 Server Actions,或者把函数定义在客户端组件内部。
  • 如果是类实例、Date等,将数据先转化为纯 JSON 兼容结构(如toISOString()toString())再传。

6.3 数据请求了两次,客户端和服务端都在拉

有人会遇到页面同时发了两次某种数据的请求,一次来自服务端组件,一次来自客户端组件。这种情况多半是因为:服务端组件先取数渲染了一部分,但页面某个客户端组件在 mount 之后又用useEffect拉了一遍相同的数据。

排查和解决技巧:

  • 优先让数据在服务端组件获取,然后通过 props / context 传给客户端组件。
  • 如果必须客户端获取,那么服务端那一侧就不要重复取数,或者用 Next.js 的缓存机制合理控制重复请求范围。
  • 也可以利用 React 的cache函数(Next.js 内置)对相同请求做去重,保证在高并发下不会重复打到数据库。

6.4 Next.js 部署后 Server Actions 请求偶发失败

本地开发一切正常,部署到生产环境后,Server Actions 偶尔返回 404 或 500。这类问题多数出在反向代理或 CDN 层面。

Server Actions 的请求本质是 POST 请求,带上了特殊的响应头标识(content type 为text/x-component)。如果你用了 CDN 或边缘缓存,把这类 POST 请求缓存了,或者代理把 header 中的关键信息丢掉了,就会导致服务端无法识别 action 身份。解决方式是在 CDN 配置中跳过 POST 请求缓存,或者对包含Next-Action-ID的 header 做透传。

6.5 服务端组件执行业务代码时遇到跨域/CORS 问题

服务端组件是运行在 Node 环境中的,所以它处理跨域的思路和浏览器完全不同。你在客户端组件里 fetch 接口可能报 CORS,但在服务端组件里直接 fetch 通常不会受 CORS 限制,因为 Node 环境没有“同源策略”。如果你在服务端组件里收到了 CORS 相关报错,第一反应应该是:这个请求是不是意外被打到客户端了?再看看代码里是不是把客户端组件误标成了服务端组件,导致浏览器执行了 fetch。

6.6 调试体验:如何看服务端组件 vs 客户端组件的渲染产物

刚开始接触 RSC 时,我也经常不确定“这段代码到底跑在哪边”。分享几个调试技巧:

  • 在服务端组件里写一个console.log,它出现在服务端终端(如 Node 进程的 stdout),而不是浏览器 DevTools。
  • 在客户端组件里写console.log,它出现在浏览器 Console 面板。
  • 如果某些日志两边都出现,说明组件被服务端预渲染了,但同时也发了客户端包去做水合。
  • 用 React DevTools 的 Profiler 可以查看组件树,服务端组件的显示和客户端组件有时会有标识区别。怀疑时,直接在组件里分别打印当前环境判断,也是最直接的方法。

7. 从 Server Components 反推前端的未来:架构思维升级

写到这儿,我想把视角拉高一点,聊聊这套技术带来的思维方式变化,而不是停留在“怎么用”的层面。

RSC 其实标志着 React 正在从一个“UI 渲染库”演进成一套“全栈组件运行时”。组件不再是只存在于浏览器里的概念,它可以穿越运行环境,按需分派到最合适的地方执行。你的页面部分代码留在服务端,部分跑到客户端,中间用 Flight 序列化协议连接,形成一条无缝链路。

对开发者来说,这意味着我们终于有了一个“概念统一”的编程模型。以前写前端要频繁思考“这数据我该在哪个阶段拿”“API 该什么时候请求”“loading 状态放哪”“SEO 怎么办”,现在组件的分层本身就在回答这些问题。服务端组件帮你确定内容层,客户端组件帮你确定交互层,边界清晰,心智负担反而降低了。

对团队协作来说,这种架构也有好处。服务端组件可以承担更多数据聚合和渲染职责,客户端组件专注于交互细节,两拨人可以在各自领域精细优化,不用在同一个文件里互相踩脚。模块边界一旦清楚,test 也更明确:服务端组件测数据渲染和异常兜底,客户端组件测交互逻辑和状态管理。

我还记得有一个项目,最初是用纯 CSR 写的报表系统,首屏要 6 秒以上,优化无从下手。后来我们用 Next.js App Router 重写,把取数、聚合、权限判断全部放服务端组件,图表和表格控件用客户端组件,首屏直接降到了 1 秒以内。最大的变化不是某个某配置项,而是整个团队在设计页面时的思考方式——先问“这段逻辑有没有必要在浏览器跑”,再动手写代码。

这种架构思维上的升级,才是 Server Components 真正留给我们的财富。


最后分享一点我的个人体验:学 RSC 不要只停留在看文档或跑 demo,最好的方式是拿一个你现有的页面,尝试把它拆分成服务端层和客户端层,亲手在浏览器 Network 面板里对比一下前后 JS 体积的变化,以及请求数量的变化。只有亲眼看到“数据不再经过 API 层直接进了组件”和“JS bundle 明显变小”这两个现象,你才会真正理解这套模型的价值。如果你正在准备 React 相关的面试,把“CSR vs RSC”“Server Components 与 SSR 的区别”“'use client' 与 'use server' 的用途”这三个问题用自己的话讲清楚,基本就能证明你理解了这个架构演进的脉络。希望这篇文章能帮你把这条脉络打通。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 19:42:21

ESP32音频播放核心原理:WAV解析、I2S时序与MicroPython实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 19:33:20

基于AT89C51的八路抢答器设计与Keil/Proteus仿真调试

简介&#xff1a;面向微机原理与接口技术课程设计的竞赛抢答器项目&#xff0c;是一份完整的课程设计文档&#xff0c;适合计算机、电子信息类专业学生完成同类综合实践时参考。文档围绕8路抢答器展开&#xff0c;覆盖总体设计、硬件电路、软件设计、仿真调试与源程序等模块&am…

作者头像 李华
网站建设 2026/9/19 19:31:15

Dify+Trae自动化生成数据清洗脚本

简介&#xff1a;本资源是一份面向Python开发者、数据分析师与数据科学家的实战型技术指南&#xff0c;聚焦利用大模型自动化生成数据预处理脚本的核心方法论。通过Trae调用Dify API构建「脚本生成助手」&#xff0c;实现从原始数据格式到目标格式的智能转换&#xff0c;覆盖Pr…

作者头像 李华