5 Differences Between React Server Components and Server-side Rendering

服务器组件服务器端渲染之间的 5 个差异

这两种方法都旨在在服务器上呈现 React 组件,但它们实际上有何不同?让我们来分析一下。

1. Bundle Size 1. 包裹大小

服务器组件的一个关键优势是减少发送到客户端的 JavaScript 数量。服务器组件在服务器上渲染,不需要任何客户端 JavaScript。它们只需将渲染结果发送到客户端。

使用服务器端渲染,整个应用程序包都被发送到客户端,尽管 HTML 已经预先渲染。这意味着客户端仍然接收到应用程序所需的所有 JavaScript,这可能导致包大小比服务器组件大。

看这个简单组件显示格式化日期的示例:

import { formatDistanceToNow, formatRelative } from 'date-fns';
 
export function ProductPriceInfo({ product }) {
  const priceUpdatedAt = new Date(product.priceUpdatedAt);
 
  const timeSinceLastUpdate = formatDistanceToNow(priceUpdatedAt, { addSuffix: true });
  const relativeDate = formatRelative(priceUpdatedAt, new Date());
 
  return (
    <div>
      <header>
        <strong>{product.name}</strong>
        <small>Last price update: {relativeDate} ({timeSinceLastUpdate})</small>
      </header>
      <p>Current price: {product.price}</p>
    </div>
  );
}

使用服务器组件时,日期格式化(通过date-fns)完全在服务器上进行。服务器将已格式化的日期字符串发送到客户端,因此无需在客户端包中包含date-fns库。请注意,服务器组件的一个额外好处是不会将其源代码发送到客户端。

相比之下,对于 SSR,虽然初始页面渲染是在服务器上生成的,但客户端仍需要重新注水应用程序 - 使其在客户端上可交互。因此,date-fns 函数仍必须包含在客户端包中。

2. Hydration Process 水合作用过程

在服务器上进行 SSR 渲染 HTML 后,客户端需要注水页面。这意味着 React 在客户端重新运行 JavaScript 以使页面可交互,这可能会引入延迟,尤其是对于较大的应用程序。

如果客户端 HTML 与服务器渲染的 HTML 不匹配,React 将抛出一个令人生畏的Hydration failed错误,并尝试重新渲染不匹配的组件,这可能会导致布局偏移或性能问题。这种情况通常发生在服务器和客户端渲染不同的数据时,例如如果您依赖于浏览器特定的 API(如localStorage或window)。这是一个棘手的问题,几乎每个服务器端渲染的应用程序最终都会遇到。

但至少我们现在有了有用的错误消息!

服务器组件完全放弃了水合过程。这是因为您的应用程序只有交互部分需要水合才能让用户改变应用程序状态。服务器组件在客户端不运行任何 JavaScript,因此它们在这种意义上也没有任何状态。

从本质上说,服务器组件允许您跳过页面非交互部分的水合作用,消除了开发工作中的痛点并提高了渲染效率。代价是您必须考虑页面哪些部分是静态的,不需要任何用户交互。

3. 组件树是如何渲染的

在服务器端渲染中,整个组件树都在服务器上渲染,然后将 HTML 发送到客户端。这里 React 通过执行客户端 JavaScript 来重新注水组件,比较服务器渲染的虚拟 DOM 与客户端版本,并启用交互性。

使用服务器组件,仅在服务器上渲染组件树的选定部分。这个想法是将组件分为服务器组件和客户端组件,允许服务器组件处理数据获取、计算和繁重的工作,而无需将这些组件的 JavaScript 发送到客户端。需要用户交互的组件(例如表单)将保留为经典的客户端组件。

科达普斯学院的一个视频很好地解释了这种差异,使用了交互性岛屿这个术语作为例子。本质上,典型网页的大部分保持静态,只有某些元素(如按钮或表单字段)需要用户交互。服务器组件架构通过将这些交互”岛屿”与服务器渲染过程隔离,消除了这些区域的需要水合作用 - 并获得了对 UI 更新的细粒度控制。正如视频中所述:

在”服务器组件”中有趣的词是”组件”,而不是”服务器”。

4. Component Lifecycle 组件生命周期

服务器端渲染的组件的功能大多与其他 React 组件类似。您仍然可以访问组件生命周期,包括useEffect或useState等钩子。在服务器发送的初始渲染之后,一旦组件在浏览器上执行,它们的功能就与常规客户端组件一样。但是,也有一些注意事项:您需要小心避免混合错误,如第二节中提到的,并记住useEffect不会在服务器上运行,它只会在组件在浏览器中重新渲染时运行。

从相反的角度来看,服务器组件是根本不同的东西。它们没有客户端生命周期,因为它们不在浏览器中运行。像useEffect或useState这样的钩子不能在它们内部使用。服务器组件主要专注于获取和渲染数据,它们不关心用户交互和副作用。一个简单的服务器组件渲染博客文章的例子:

export default async function BlogPosts() {
  const posts = await fetchPosts(); // in server components we can use await directly in the function body
 
  return (
    <div>
      <h1>Latest Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

当使用服务器端渲染时,您通常不需要太担心哪些组件是交互式的。相比之下,服务器组件对此要严格得多。如果您后来决定为服务器组件添加交互性,您很可能面临应用程序重构的挑战

5. Data Fetching 数据获取

服务器端渲染通常需要两种策略来获取数据。假设您需要渲染用户列表,但仍允许从浏览器进行分页。首先,我们需要提供初始批量数据,通常通过函数(如 Next 的getStaticProps或getServerSideProps)传递给 props(这将是从服务器传递到浏览器的数据)。第二是在客户端获取用户请求的数据(获取用户列表的后续页面)。

Simple example of this approach from TanStack Query’s documentation:
这是一个简单的示例,来自TanStack Query 的文档:

export async function getStaticProps() {
  const users = await fetchUsers();
  return { props: { users } };
}
 
function UserList(props) {
  const { data: users } = useQuery({ // \`users\` variable will contain current data reacting to User actions
    queryKey: ['users'],
    queryFn: fetchUsers,
    initialData: props.users, // pass static prop as an initial data to the cache
  })
}

使用服务器组件,无需额外的复杂性,因为输出是静态的,不会被客户端用户改变。这简化了处理边缘情况的过程。如果需要,您仍然可以使用 React 的缓存函数等实用程序来优化数据获取,确保跨多个组件重复使用相同的数据。

Final Thoughts 最后的想法

服务器端渲染和服务器组件是构建 React 应用程序的两种强大技术。虽然服务器组件是前端开发工具包的新增功能,但它们并不是服务器端渲染的替代品。事实上,它们可以相互补充,各自满足不同的需求。了解这些方法之间的关键差异将有助于您决定何时以及如何使用它们,从而最大限度地提高 React 项目的性能和可维护性。

Tymek Zapała

Written by Tymek Zapała 由 Tymek Zapała 撰写