专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

React 官方为何坚持推出反常规的RSC组件?

ins518 2025-06-30 16:45:44 技术文章 7 ℃ 0 评论

家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

为什么需要服务器组件

React 服务器组件 (React Server Components,即RSC) 扩展了 React 的边界,使得 React 超越了纯粹的渲染库范畴,将数据获取和远程客户端服务器通信纳入框架内。

在 React 之前,开发者可以使用 PHP、JSP 等脚本语言在客户端和服务器之间建立更紧密的关系,比如:开发者可以访问服务器以直接在当前页面内调用数据。 然而,它也存在诸多缺点,比如由于跨团队依赖和高流量需求而难以扩展单体应用程序。

单体应用(Monolithic Application)是一种传统的应用程序架构模式,其中整个应用程序作为一个单一、独立的单元进行开发、部署和运行。在单体应用中,所有的功能模块和业务逻辑都集中在一个应用程序中,通常使用统一的数据库和中间件。

React 是为了可组合性增量采用现有代码库而创建。 为了响应丰富交互性的世界,它解耦了客户端和服务器的关注点,使前端的组合更加灵活。比如:由不同的开发人员开发开发的两个 React 组件可以一起工作,因为在同一个框架中运行。

为了实现这一目标,React 必须在现有的 Web 标准之上进行创新。 经过过去多页MPA 和单页SPA、客户端渲染和服务器端渲染的演变,目标始终保持不变:即提供快速数据获取、丰富的交互性并改善开发者体验。

服务端渲染和 React Suspense 解决了什么

在服务器组件的发展道路上,还有诸多问题亟需解决。 为了更好地理解 RSC ,需要先熟悉下服务器端渲染 (SSR) 和 Suspense 。

SSR 专注于初始页面加载,即将预渲染的 HTML 发送到客户端,然后使用下载的 JavaScript 进行水合,从而使得整个应用像典型的 React 应用程序一样运行。 同时,SSR 也只发生一次,即当用户直接导航到当前页面时。

使用 SSR,用户浏览器可以更快地获取 HTML,但是依然必须等待“全有或全无(all-or-nothing)”瀑布才能与 JavaScript 交互:

  • 在渲染任何数据之前,必须从服务器获取所有数据。
  • 所有 JavaScript 必须先从服务器下载,然后客户端才能使用。
  • 所有水合作用都必须在客户端完成,然后才能进行交互。

为了解决这个问题,React 创建了 Suspense,Suspense 组件允许服务器端 HTML 流和客户端上的选择性水合作用。 通过使用 <Suspense> 组件,开发者可以告诉服务器暂停该组件的渲染和水合作用,从而让其他组件优先加载而不会被较重的组件阻塞。

当开发者在 <Suspense> 中有多个组件时,React 会按照编写的组件顺序沿组件树向下工作,从而允许在应用程序中以最佳方式进行流式传输。 但是,如果用户尝试与某个组件交互,则该组件将优先于其他组件。

import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';
//在当前示例中,Biography和Albums都获取一些数据。然而,由于它们被分组在单个 Suspense
//边界下,因此这些组件总是同时在一起“弹出”
export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Biography artistId={artist.id} />
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
      </Suspense>
    </>
  );
}
function Loading() {
  return <h2> Loading...</h2>;
}

Suspense和选择性水合极大地改善了客户端渲染的性能,但仍然留下了一些潜在问题:

  • 在渲染组件之前,必须从服务器获取整个页面的数据。 解决这个问题的唯一方法是在 useEffect() 钩子中获取客户端数据,该钩子的往返时间比服务器端获取的时间更长,并且仅在组件渲染和水化之后发生。
  • 所有页面 JavaScript 最终都会被下载,即使它是异步流式传输到浏览器的。 随着应用程序复杂性的增加,用户下载的代码量也会增加。
  • 尽管优化了水合作用,但在下载并为组件实现客户端 JavaScript 之前,用户仍然无法与组件交互。
  • 大多数 JavaScript 计算量仍然在客户端上,客户端可能在任何类型的设备上运行,丧失了更强大、可预测的服务器渲染。

下图表示了没有React服务器组件的示例:

React 服务器组件作用

为了解决上述问题,React 创建了 Server Components, RSC 单独获取数据并完全在服务器上渲染,生成的 HTML 会流入客户端 React 组件树,并根据需要与其他服务器和客户端组件一起渲染。

// Note.js - Server Component
import db from 'db'; 
// (A1) We import from NoteEditor.js - a Client Component.
import NoteEditor from 'NoteEditor';
async function Note(props) {
  const {id, isEditing} = props;
  // (B) Can directly access server data sources during render, e.g. databases
  const note = await db.posts.get(id);
  return (
    <div>
      <h1>{note.title}</h1>
      <section>{note.body}</section>
      {/* (A2) Dynamically render the editor only if necessary */}
      {isEditing 
        ? <NoteEditor note={note} />
        : null
      }
    </div>
  );
}

此过程消除了客户端重新渲染的需要,从而提高了性能。 对于任何客户端组件,水合作用可以与 RSC 同时发生,因为计算 payload 在客户端和服务器之间共享。

换句话说,服务器功能更加强大,并且在物理上更接近数据源,它处理计算密集型渲染,并仅向客户端发送交互式代码片段

当 RSC 由于状态更改而需要重新渲染时,它会在服务器上刷新并无缝合并到现有 DOM 中,而无需硬刷新。 因此,即使从服务器更新了部分视图,客户端状态也会被保留。

RSC:性能和包大小

RSC 可以显著减小客户端 JavaScript 包的大小并提高加载性能。

传统上,在浏览应用程序时,客户端会下载并执行所有代码和数据依赖项,如果框架没有代码分割功能,很多无关代码也会被加载。

但是,RSC 解决了服务器上的所有依赖关系,更接近应用程序数据的来源。 RSC 只在服务器上渲染代码,这比客户端渲染任务要快得多。 同时,React 仅将这些处理结果加上客户端组件发送到浏览器。

换句话说,使用服务器组件 RSC,初始页面加载更快、更精简。 客户端运行时的大小是可缓存且可预测的,并且不会随着应用程序的增长而暴增。 添加额外的面向用户的 JavaScript 主要是因为应用程序需要通过客户端组件进行更多客户端交互。

RSC:集成Suspense

RSC 组件可以在同一个 React 树中与客户端组件同时渲染。 通过将大部分应用程序代码移至服务器,RSC 有助于防止客户端数据获取瀑布流,从而快速解决服务器端的数据依赖性。

在传统的客户端渲染中,组件使用 React Suspense 来“暂停”其渲染过程(并显示回退状态),同时等待异步工作完成。 使用 RSC,数据获取和渲染都发生在服务器上,因此 Suspense 也在服务器端等待,从而缩短了总往返时间,以加快渲染回退和完成页面的速度

// Tweets.server.js
import { fetch } from 'react-fetch' 
// React's Suspense-aware fetch()
import Tweet from './Tweet.client'
export default function Tweets() {
  const tweets = fetch(`/tweets`).json()
  return (
    <ul>
      {tweets.slice(0, 2).map((tweet) => (
        <li>
          <Tweet tweet={tweet} />
        </li>
      ))}
    </ul>
  )
}
// Tweet.client.js
export default function Tweet({ tweet }) {
  return <div onClick={() => alert(`Written by ${tweet.username}`)}>{tweet.body}</div>
}
// OuterServerComponent.server.js
export default function OuterServerComponent() {
  return (
    <ClientComponent>
      <ServerComponent />
      <Suspense fallback={'Loading tweets...'}>
        <Tweets />
      </Suspense>
    </ClientComponent>
  )
}

值得注意的是,客户端组件在初始加载时仍然是 SSR 的。 RSC 模型不会取代 SSR 或 Suspense,而是与它们一起工作,根据用户的需要提供应用程序的所有部分。

在带有 React Server 组件的 Next.js 中,数据获取和 UI 渲染可在同一个组件完成。此外,还为用户提供了一种在 JavaScript 加载到页面之前与服务器端数据进行交互的方法

RSC:局限性

为服务器组件编写的所有代码都必须是可序列化的,这意味着不能使用生命周期 Hooks,例如 useEffect() 或 state。

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);
  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

但是,开发者仍然可以通过服务器操作从客户端与服务器进行交互。此外,RSC 不支持连续更新,例如通过 WebSocket。 在这些情况下,需要客户端获取或轮询方法。

如何使用 React 服务器组件

RSC 的优点在于开发者不需要完全了解工作原理即可使用。 Next.js 13.4 中引入的 App Router 提供了功能最齐全的 RSC 实现,默认情况下所有组件都是服务器组件。

如果想使用生命周期事件,例如 useEffect() 或 state,则需要编写客户端组件。 选择加入客户端组件只需在组件顶部声明“use client”即可。

'use client';
import { useState } from 'react';
export default function RichTextEditor(props) {
  // ...

平衡服务器和客户端组件

值得注意的是,RSC 并不是要取代客户端组件,应用程序利用 RSC 进行动态数据获取,并利用客户端组件来实现丰富的交互性。

作为开发人员,可以考虑利用 RSC 进行服务器端渲染和数据获取,同时依靠客户端组件来实现本地交互功能和用户体验。 通过取得适当的平衡,从而可以创建高性能、高效的应用程序。

最重要的是,开发者可以继续在非标准环境中测试应用程序:模拟较慢的电脑、较慢的手机和较慢的 WiFi。当然,RSC 并不是解决用户过多客户端 JavaScript 负担问题的完整解决方案,但确实可以让开发者选择何时将计算量转移到用户设备上。

使用 Next.js 改进数据获取

RSC 在服务器上获取数据,不仅提供对后端数据的安全访问,还通过减少服务器与客户端的交互来提高性能。 与 Next.js 的增强相结合,RSC 还允许智能数据缓存、单次往返中的多次提取以及自动 fetch() 请求重复数据删除,所有这些都最大限度地提高了客户端发送数据的效率。

最重要的是,在服务器上获取数据有助于防止客户端数据获取瀑布,比如:请求相互堆积,并且必须在用户继续操作之前串行解决。 服务器端获取的开销要小得多,因为它们不会阻塞整个客户端,并且解析速度更快。

此外,开发者不再需要 Next.js 特定的页面级方法,例如: getServerSideProps() 和 getStaticProps(),这些方法无法为各个组件提供足够精细的控制,并且往往会过度获取数据

在 Next.js App Router 中,所有获取的数据现在默认都是静态的,在构建时渲染。 但是,开发者可以轻松更改。Next.js 扩展了获取选项对象以提供缓存和重新验证规则的灵活性。

可以使用 {next: {revalidate: number}} 选项以设定的时间间隔或当后端发生更改时刷新静态数据(增量静态重新生成),而 {cache: 'no-store'} 选项可以在 fetch 中传递请求动态数据(服务器端渲染)。

所有这一切使得 Next.js App Router 中的 React Server 组件成为高效、安全和动态数据获取的强大工具,所有这些组件都默认缓存以提供高性能的用户体验。

服务器操作:React 迈向可变性的第一步

在 RSC 的上下文中,服务器操作是在服务器端的 RSC 中定义的函数,然后可以跨服务器/客户端边界传递该函数。 当用户在客户端与应用程序交互时,可以直接调用将在服务器端安全执行的服务器操作

此方法在客户端和服务器之间提供无缝的远程过程调用 (RPC) 体验,开发者可以直接从客户端组件调用服务器操作,而不是编写单独的 API 路由来与服务器通信。

还要记住,Next.js App Router 围绕智能数据缓存、重新验证和变异构建。 Next.js 中的服务器操作意味着开发者可以在对服务器的同一个往返请求中改变缓存并更新 React 树,同时通过导航保持客户端缓存的完整性。

具体来说,服务器操作旨在处理数据库更新或表单提交等任务。 例如:逐步增强表单,这意味着即使 JavaScript 尚未加载,用户仍然可以与表单交互,并且服务器操作将处理表单数据的提交和处理。

Server Actions 提供的机会(无论是渐进增强还是消除 API 开发工作)对于可访问性、可用性和开发人员体验都非常有用。

参考资料

https://vercel.com/blog/understanding-react-server-components

http://cd.mobiletrain.org/ganhuo/121459.html

https://react.dev/reference/react/use-client

https://legacy.reactjs.org/docs/hooks-effect.html

https://dev.to/xakrume/react-server-components-deep-dive-29af

https://www.plasmic.app/blog/how-react-server-components-work

https://www.patterns.dev/posts/react-server-components

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表