译:为何 React Server Components 正在破坏性构建以赢得未来

作者:Vishwas Gopinath 原文:https://www.builder.io/blog/why-react-server-components

在过去的十年中,React及其生态系统经历了不断的演变。每个版本都引入了新的概念、优化,有时甚至是范式的转变,推动了我们在Web开发中认为可能的界限。

React Server Components(RSC)是自React hooks以来最新且可能是最重大的变化。然而,这一变化在社区中引起了不同的反应。

对我而言,Linkin Park的这句歌词捕捉到了我们步入2024年时围绕React演变的情感:

Cause once you got a theory of how the thing works Everybody wants the next thing to be just like the first (因为一旦你有了关于事物如何工作的理论,每个人都希望下一件事情与第一件事情完全相同。)

我们已经习惯了我们熟悉和喜爱的React,因此理解并接受这种范式转变,理所当然地会带来犹豫和怀疑。

本博客的目标是带领你穿越React多年来的渲染演变之旅,并帮助你理解为什么 React Server Components 不仅是不可避免的,而且还是构建经济高性能React应用程序、提供卓越用户体验的未来。

客户端渲染(CSR)

如果你在开发领域待了一段时间,你会记得React是创建单页应用程序(SPA)的首选库。

在典型的SPA中,当客户端发出请求时,服务器将一个单独的HTML页面发送到浏览器(客户端)。该HTML页面通常只包含一个简单的div标签和对JavaScript文件的引用。这个JavaScript文件包含应用程序运行所需的一切,包括React库本身和应用程序代码。它在解析HTML文件时下载。

然后,下载的JavaScript代码在计算机上生成HTML,并将其插入到DOM的根div元素下,你就可以在浏览器中看到用户界面。

当你在DOM检查器中看到HTML出现但在查看源代码选项中看不到HTML文件时,就能看到这个过程。

这种渲染方法,其中组件代码直接在浏览器(客户端)内部转换为用户界面,被称为客户端渲染(CSR)。

以下是客户端渲染的可视化效果:

以下是React SPA的DOM检查器与页面源代码的对比:

CSR DOM vs Source

CSR迅速成为SPA的标准,得到了广泛的采用。然而,不久之后,开发人员开始注意到这种方法的一些固有缺点。

CSR的缺点

首先,生成主要包含一个div标签的HTML对于SEO来说并不理想,因为它为搜索引擎提供了很少的内容进行索引。深度嵌套组件的大捆绑大小和从API响应中进行网络请求的瀑布流可能导致有意义的内容渲染不足以供爬虫快速索引。

其次,让浏览器(客户端)处理所有工作,如获取数据、计算UI和使HTML具有交互性,可能会减慢页面加载速度。用户可能会看到一个空白屏幕或加载旋转器,而页面加载。随着每个新功能添加到应用程序,JavaScript捆绑包的大小增加,延长了用户查看UI的等待时间。对于互联网连接较慢的用户,这种问题尤为明显。

CSR为我们习惯的交互式Web应用程序奠定了基础,但为了提高SEO和性能,开发人员开始寻找更好的解决方案。

服务器端渲染(SSR)

为了克服CSR的缺点,现代React框架如Next.js转向了服务器端解决方案。这种方法从根本上改变了向用户提供内容的方式。

与其发送一个几乎为空的HTML文件,依赖于客户端JavaScript构建页面,服务器负责渲染完整的HTML。这个完全形成的HTML文档然后直接发送到浏览器。由于HTML是在服务器上生成的,浏览器能够快速解析和显示它,从而提高初始页面加载时间。

以下是服务器端渲染的可视化效果:

解决CSR的缺点

服务器端方法有效地解决了CSR的问题。

首先,它极大地改善了SEO,因为搜索引擎可以轻松地索引由服务器渲染的内容。

其次,浏览器可以立即加载页面HTML内容,而不是一个空白屏幕或加载旋转器。

Hydration

SSR对立即提高内容的可见性的方法在其自身的复杂性方面,尤其是在页面的交互性方面,具有一定的复杂性。在JavaScript捆绑包(包括React本身以及应用程序特定的代码)被浏览器完全下载和执行之前,页面的完整交互性被搁置。

这个重要的阶段被称为 Hydration,是在服务器最初提供的静态页面的基础上给页面带来生命的过程。在 Hydration 过程中,React在浏览器中接管,根据提供的静态HTML在内存中重建组件树。它仔细规划了在该树中放置交互元素的位置。

然后,React继续将必要的JavaScript逻辑绑定到这些元素。这包括初始化应用程序状态、为诸如点击和悬停等操作附加事件处理程序,以及设置任何其他用于完全交互用户体验的动态功能。

SSG和SSR

更深入地说,服务器端解决方案可以分为两种策略:静态站点生成(SSG)和服务器端渲染(SSR)。

SSG发生在构建时,当应用程序部署到服务器上时。这导致已经呈现并准备好提供的页面。它非常适合很少更改的内容,比如博客文章。

另一方面,SSR是根据用户请求按需呈现页面的。它适用于个性化内容,比如社交媒体动态,其中HTML依赖于已登录的用户。通常,你会看到这两者被统称为服务器端渲染或SSR。

服务器端渲染(SSR)是对客户端渲染(CSR)的重大改进,提供更快的初始页面加载和更好的SEO。然而,SSR引入了自己的一套挑战。

SSR的缺点

SSR的一个问题是,组件不能开始渲染,然后暂停或“等待”,同时数据仍在加载中。如果组件需要从数据库或其他源(如API)获取数据,那么必须在服务器开始呈现页面之前完成这个获取。这可能会延迟服务器对浏览器的响应时间,因为服务器必须在可以将页面的任何部分发送到客户端之前完成收集所有必要的数据。

第二个问题是,为了成功的 Hydration,React在浏览器中生成的组件树必须与服务器生成的组件树完全相匹配。这意味着在可以开始 Hydration 其中任何组件之前,必须在客户端加载所有组件的JavaScript。

SSR的第三个问题与 Hydration 本身有关。React以一次传递的方式 Hydration 组件树,这意味着一旦它开始 Hydration,它就不会停止,直到完成整个树的 Hydration。因此,在你可以与任何组件交互之前,必须对所有组件进行 Hydration。

这三个问题——必须加载整个页面的数据,加载整个页面的JavaScript,以及 Hydration 整个页面——在从服务器到客户端的整个流程中创建了一个全有或全无的瀑布问题,在解决一个问题之前必须解决下一个问题。如果应用程序的某些部分比其他部分慢,这在实际应用程序中经常是这样的情况,这是效率低下的。

由于这些限制,React团队引入了一种新的改进的SSR架构。

服务器端渲染悬挂

React 18引入了SSR的Suspense以解决传统SSR的性能缺陷。这种新的架构允许你使用[<Suspense](https://react.dev/reference/react/Suspense)>组件解锁两个主要的SSR功能:

  1. 服务器端HTML流
  2. 客户端选择性 Hydration

服务器端HTML流

正如我们在前一部分中讨论的,传统上,SSR一直是全有或全无的事务。服务器渲染完整的HTML,然后发送到客户端。客户端显示这个HTML,只有在完整的JavaScript捆绑包加载后,React才会继续 Hydration 整个应用程序以添加交互性。

以下是上述过程的可视化效果:

然而,使用React 18,我们有了一个新的可能性。通过在页面的一部分,例如主内容区域,包装在React的Suspense组件内,我们告诉React它不需要等待主部分的数据被获取才开始流动页面的其余部分的HTML。React会发送一个占位符,比如一个加载旋转器,而不是完整的内容。

一旦服务器准备好主部分的数据,React通过正在进行的流动,发送附加的HTML,带有内联的<script>标签,其中包含正确定位该HTML所需的最小JavaScript。因此,即使在完整的React库在客户端加载之前,主部分的HTML对用户也变得可见。

以下是使用<Suspense>进行HTML流的可视化效果:

这解决了我们的第一个问题。你不必等待一切都下载后才能显示任何内容。如果特定部分延迟了初始HTML,它可以后来无缝集成到流中。这就是<Suspense>如何促进服务器端HTML流的要点。

客户端选择性 Hydration

虽然我们现在可以加速初始HTML的交付,但我们仍然有另一个挑战。在加载主部分的JavaScript之前,客户端应用程序 Hydration 不能开始。如果主部分的JavaScript捆绑包很大,这可能会显著延迟该过程。

为了减轻这个问题,可以使用代码拆分。代码拆分意味着你可以将特定的代码段标记为不立即需要加载,向你的捆绑器发出信号,将它们分隔到单独的<script>标签中。

使用[React.lazy](https://react.dev/reference/react/lazy)进行代码拆分,使你能够将主部分的代码与主JavaScript捆绑包分开。结果,包含React和整个应用程序的代码的JavaScript可以独立于主部分的代码之前由客户端下载,无需等待主部分的代码。

这是至关重要的,因为通过在<Suspense>中包装主部分,你告诉React它不应该阻止页面的其余部分不仅流式传输,而且从 Hydration 的过程中流式传输。这个功能称为选择性 Hydration ,允许在完整的HTML和JavaScript代码完全下载之前,逐步 Hydration 主部分之前, Hydration 部分 Hydration。

从用户的角度来看,最初他们会得到作为HTML流的非交互内容。然后,你告诉React Hydration。主部分的JavaScript代码还没有到位,但没关系,因为我们可以选择性地 Hydration 其他组件。

一旦主部分的代码加载完毕,主部分就被 Hydration 了。

由于选择性 Hydration ,一个庞大的JS片段不会阻止页面的其余部分变得交互。

以下是使用<Suspense>进行选择性 Hydration 的可视化效果:

此外,选择性 Hydration 为第三个问题提供了解决方案:必须“使一切 Hydration 以与任何东西互动”。React在尽可能早地开始 Hydration 的同时,使交互元素,如标题和侧边导航等,能够立即响应用户交互。

在等待主内容 Hydration 之前,此过程由React自动管理。

在等待多个组件 Hydration 的情况下,React根据用户交互的情况优先 Hydration 。例如,如果侧边栏即将被 Hydration ,而你点击了主内容区域,React将在点击事件的捕获阶段同步 Hydration 点击的组件。这确保组件立即准备好响应用户交互。然后,sidenav稍后再被 Hydration 。

以下是根据用户交互进行 Hydration 的可视化效果:

Suspense for SSR的缺点

首先,即使JavaScript代码以异步方式流式传输到浏览器,最终用户仍然必须下载整个网页的代码。随着应用程序添加更多功能,用户需要下载的代码量也会增加。这带来一个重要的问题:用户是否真的需要下载这么多数据?

其次,当前的方法要求所有React组件在客户端被 Hydration ,而不考虑它们实际上是否需要交互性。此过程可能会浪费资源,并延长用户的加载时间和交互时间,因为它们的设备需要处理和呈现可能甚至不需要客户端交互的组件。这带来了另一个问题:是否所有组件都应该被 Hydration ,即使它们不需要交互?

第三,尽管服务器在处理密集的处理任务方面具有更强大的能力,但绝大部分JavaScript执行仍然发生在用户的设备上。这可能会降低性能,尤其是在性能不是很强大的设备上。这带来了另一个重要的问题:是否应该在用户的设备上完成这么多的工作?

要解决这些挑战,仅仅迈出渐进的步骤是不够的。我们需要朝着更强大的解决方案迈出重大的一步。

React Server Components(RSC)

React Server Components(RSC)代表React团队设计的一种新架构。这种方法旨在利用服务器和客户端环境的优势,优化效率、加载时间和交互性。

该架构引入了一个双组件模型,区分了客户端组件和服务器组件。这个区别不是基于组件的功能,而是基于它们执行的位置和它们设计用于与之交互的特定环境。让我们更仔细地看看这两种类型:

客户端组件

客户端组件是我们在先前的渲染技术中一直在使用和讨论的熟悉的React组件。它们通常在客户端(CSR)上呈现,但它们也可以在服务器上(SSR)呈现为HTML,允许用户立即看到页面的HTML内容而不是空白屏幕。

“客户端组件”的概念可能看起来令人困惑,但将它们视为主要在客户端上运行但(并且应该)也在服务器上执行一次作为优化策略可能会有所帮助。

客户端组件可以访问客户端环境,比如浏览器,使它们能够使用状态、效果和事件监听器来处理交互,还可以访问浏览器专用的API,比如地理位置或localStorage,使你能够为特定用例构建前端,就像在引入RSC架构之前的所有这些年里一样。

事实上,“客户端组件”这个术语并没有表示任何新的React功能或概念。它简单地将我们之前就用过的React组件纳入一个统一的术语,以在RSC中对它们进行区分。

下面是一个 Counter 客户端组件的例子:

"use client"

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>Counter</h2>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Server Components

Server Components 代表一种新型的React组件,专门设计用于仅在服务器上运行。与客户端组件不同,它们的代码保留在服务器上,并且永远不会下载到客户端。这种设计选择为React应用程序提供了多个好处。让我们更详细地了解这些好处。

零 Bundle 大小 首先,在 Bundle 大小方面,Server Components 不会将代码发送到客户端,从而使大型依赖项保持在服务器端。这使得在网络连接较慢或设备性能较差的用户中,无需为这些组件下载、解析和执行JavaScript。此外,它消除了水合步骤,加速了应用程序的加载和交互。

直接访问服务器端资源 其次,通过直接访问后端服务器资源(如数据库或文件系统),Server Components 实现了在不需要额外客户端处理的情况下进行高效的数据获取和渲染。通过利用服务器的计算能力和对数据源的接近性,它们管理计算密集型的渲染任务,并仅向客户端发送可交互的代码片段。

增强的安全性 第三,Server Components 的独占服务器端执行增强了安全性,将敏感数据和逻辑(包括令牌和API密钥)远离客户端。

改进的数据获取 第四,Server Components 增强了数据获取的效率。通常,在客户端使用useEffect获取数据时,子组件在父组件完成加载其数据之前无法开始加载其自己的数据。这种数据的顺序获取通常导致性能不佳。

主要问题不是往返通信本身,而是这些往返通信是从客户端到服务器端进行的。Server Components 使应用程序能够将这些顺序往返通信转移到服务器端。通过将此逻辑移至服务器,减少了请求延迟,提高了整体性能,消除了客户端-服务器瀑布。

缓存 第五,服务器端渲染使结果可以进行缓存,并且可以在后续请求和不同用户之间重复使用。这种方法可以通过最小化每个请求所需的渲染和数据获取,显着提高性能并降低成本。

更快的初始页面加载和首次内容呈现 第六,使用Server Components ,初始页面加载和首次内容呈现(FCP)显着得到改善。通过在服务器上生成HTML,页面立即呈现,无需等待JavaScript的下载、解析和执行。

改进的搜索引擎优化 第七,就搜索引擎优化(SEO)而言,服务器渲染的HTML对搜索引擎机器人完全可访问,增强了页面的可索引性。

高效的流式传输 最后,有流式传输。Server Components 允许将渲染过程分成可管理的块,然后在准备就绪时将其作为流传输到客户端。这种方法允许用户更早地开始看到页面的某些部分,而无需等待整个页面在服务器上完成渲染。

以下是一个ProductList页面 Server Components 的示例:

export default async function ProductList() {
  const res = await fetch("https://api.example.com/products");
  const products = res.json();

  return (
    <main>
      <h1>Products</h1>
      {products.length > 0 ? (
        <ul>
          {products.map((product) => (
            <li key={product.id}>
              {product.name} - ${product.price}
            </li>
          ))}
        </ul>
      ) : (
        <p>No products found.</p>
      )}
    </main>
  );
}

“use client” 指令

在React Server Components范例中,需要注意的是,默认情况下,Next.js应用程序中的每个组件都被视为 Server Components。

要定义客户端组件,我们必须在文件顶部包含一个指令 - 换句话说,一个特殊的指示 - "use client"。此指令充当我们从服务器端到客户端跨越边界的通行证,并允许我们定义客户端组件。

它向捆绑器发出信号,表明此组件及其导入的任何组件都用于客户端执行。因此,该组件完全可以访问浏览器API并具有处理交互的能力。

“use server”指令标记了可以从客户端代码调用的服务器端函数。我们将在另一篇文章中介绍“use server”和服务器端操作。

React Server Components 渲染生命周期

让我们探讨一下React Server Components(RSC)的渲染生命周期,假设使用Next.js作为React框架。

Vercel的Next.js 13首次支持React Server Components(RSC)架构。

对于React Server Components(RSC),重要的是要考虑三个元素:你的浏览器(客户端)、服务器端的Next.js(框架)和React(库)。

初始加载序列

  • 当浏览器请求页面时,Next.js应用程序路由器将请求的URL与 Server Components 匹配。然后,Next.js指示React渲染该 Server Components。
  • React渲染 Server Components 及其任何子组件(这些子组件也是Server Components),将它们转换为一种称为RSC负载的特殊JSON格式。如果任何Server Components暂停,React暂停该子树的渲染,并发送占位符值。
  • 与此同时,客户端组件会在生命周期的稍后阶段准备好。
  • Next.js使用RSC负载和客户端组件JavaScript指令生成服务器上的HTML。该HTML被流式传输到浏览器,以立即显示路由的快速、非交互式预览。
  • 与此同时,Next.js将RSC负载作为React渲染每个UI单元的一部分进行流式传输。
  • 在浏览器中,Next.js处理流式传输的React响应。React使用RSC负载和客户端组件的指令逐步渲染UI。
  • 一旦所有客户端组件和Server Components的输出加载完毕,最终的UI状态将呈现给用户。
  • 客户端组件经历水合作用,将我们的应用程序从静态显示转变为交互式体验。

这是初始加载序列。接下来,让我们看看刷新应用程序部分的更新序列。

更新序列

  • 浏览器请求重新获取特定UI的请求,例如完整路由。
  • Next.js处理请求并将其与所请求的Server Components匹配。Next.js指示React渲染组件树。React渲染组件,类似于初始加载。
  • 但是,与初始序列不同,更新时不会进行HTML生成。Next.js将响应数据以流的形式返回给客户端。
  • 在接收到流式响应后,Next.js使用新输出触发路由的重新渲染。
  • React协调(合并)新的渲染输出与屏幕上现有的组件。由于UI描述是一种特殊的JSON格式而不是HTML,React可以更新DOM,同时保留关键的UI状态,如焦点或输入值。

这就是在Next.js中使用App Router时RSC渲染生命周期的精髓。

使用React Server Components架构,Server Components负责数据获取和静态渲染,而客户端组件负责渲染应用程序的交互元素。

总的来说,RSC架构使React应用程序能够充分利用服务器和客户端渲染的最佳方面,同时使用单一语言、单一框架和一致的API集。RSC改进了传统渲染技术,同时克服了它们的局限性。

要获取更多关于RSC的上下文和更全面的思维模型,请参阅Next.js文档或观看我的Next.js教程

2024-03-05

访问量 56

扫码关注公众号“前端微志”

第一时间获取新周刊

预览图片