2020 年 12 月 21 日,react 官方发布了一个实验性的功能Server Components,目前正属于rfc阶段,让我们一起来看一下Server Components是干嘛的?

# Server Components 有哪些特点?

  • Server Components仅在服务器上运行,对 bundle-size 的影响为零。他们的代码永远不会下载到客户端,这有助于减小程序包的大小并缩短启动时间。
  • Server Components可以访问服务器端数据源,例如数据库,文件系统或(微)服务。
  • Server Components与客户端组件(即传统的 React 组件)无缝集成Server Components可以将数据加载到服务器上并将其作为 props 传递给客户端组件,从而允许客户端处理呈现页面的交互式部分。
  • Server Components可以动态选择要渲染的客户端组件,从而允许客户端仅下载呈现渲染所需的最少代码。
  • 重新加载时,Server Components会保留客户端状态。这意味着在重新获取Server Components树时,不会破坏或重置客户端状态,焦点甚至正在进行的动画。
  • Server Components渐进式的,并将 UI 渲染的单元逐步传输到客户端。与 Suspense 结合使用,这使开发人员可以制定有意的加载状态,并在等待页面其余部分加载时快速显示重要内容。
  • 开发人员还可以在服务器和客户端之间共享代码,从而允许使用单个组件在一条路径上呈现服务器上某些内容的静态版本,而在另一条路径上呈现客户端上该内容的可编辑版本

# Zero Bundle Size Components

JavaScript 中有大量的 npm 包,然而很多包我们只使用了其中的一部分功能,却在打包时将其全量打入;这势必会造成整个 js 体积较大; Tree-shaking可以用来排除掉那些我们不需要的代码,但我们仍然最终不得不向用户提供一些额外的代码。以渲染 markdown 为例:

// NoteWithMarkdown.js
// NOTE: *before* Server Components

import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

在传统的打包中我们会将markedsanitize-html打包进去,体积大约 240K,这势必会造成包的体积较大。

当然,我们不想向让户下载那么多代码,如果您只想简单渲染Markdown,而暂时还没有交互的需求,使用Server Components或许是一种比较好的方案。

// NoteWithMarkdown.server.js - Server Component === zero bundle size

import marked from 'marked'; // zero bundle size
import sanitizeHtml from 'sanitize-html'; // zero bundle size

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

注意的是文件扩展名是“ server.js”。和客户端代码库是一样的,只是现在一切都在服务器上进行了。客户端没有将 Markdown 库的全部代码发送给客户端,而是仅获得了渲染后的结果。总之,Server Components不会影响我们前端应用程序包的大小。该代码仅在后端运行,对用户不可见。

# Server Components可以访问后端程序

Server Components的所有 React 代码都在服务端执行,类似于 Next.js,我们可以访问后端数据库,文件系统等;

// Note.server.js - Server Component
import fs from "react-fs";

function Note({ id }) {
  const note = JSON.parse(fs.readFile(`${id}.json`));
  return <NoteWithMarkdown note={note} />;
}

也可以直接后端访问来使用数据库,内部(微)服务和其他仅后端数据源:

// Note.server.js - Server Component
import db from "db.server";

function Note({ id }) {
  const note = db.notes.get(id);
  return <NoteWithMarkdown note={note} />;
}

# Auto Code Spliting

如果您使用 React或者Vue 已有一段时间,那么您可能会熟悉代码拆分的概念,它使开发人员可以将其应用程序分解为较小的 bundle,并将更少的代码发送给客户端。常见方法是在运行时根据某些规则延迟加载 bundle 和/或延迟加载不同的 module。例如,根据用户,内容,功能等,应用程序可能会延迟加载不同的代码(在不同的包中):

// PhotoRenderer.js
// NOTE: *before* Server Components

import React from "react";

// one of these will start loading *when rendered on the client*:
const OldPhotoRenderer = React.lazy(() => import("./OldPhotoRenderer.js"));
const NewPhotoRenderer = React.lazy(() => import("./NewPhotoRenderer.js"));

function Photo(props) {
  // Switch on feature flags, logged in/out, type of content, etc:
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {...props} />;
  } else {
    return <PhotoRenderer {...props} />;
  }
}

代码拆分对提高性能非常有帮助,但是现有的代码拆分方法有两个主要限制。

  • 首先,开发人员必须记住要用 React.lazy动态import替换常规的导入语句;
  • 其次,这种方法延迟了应用程序可以开始加载所选组件的时间,从而丢失了加载更少代码的某些好处

Server Components以两种方式解决这些限制。首先,它们使代码拆分自动进行:

  • Server Components将所有客户端组件的导入都视为潜在的代码拆分点。
  • 开发人员可以选择在服务器上更早使用哪个组件,以便客户端可以在渲染过程中更早地下载它。最终结果是Server Components使开发人员可以将更多的精力放在他们的应用程序上并编写自然代码,而框架默认情况下会优化应用程序
// PhotoRenderer.server.js - Server Component

import React from "react";

// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from "./OldPhotoRenderer.client.js";
import NewPhotoRenderer from "./NewPhotoRenderer.client.js";

function Photo(props) {
  // Switch on feature flags, logged in/out, type of content, etc:
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {...props} />;
  } else {
    return <PhotoRenderer {...props} />;
  }
}

# No Waterfalls

当应用程序发出串行请求以获取数据时,就会出现性能不佳。例如,数据获取的一种模式是首先呈现一个占位符,然后在 useEffect()挂钩中获取数据:

// Note.js
// NOTE: *before* Server Components

function Note(props) {
  const [note, setNote] = useState(null);
  useEffect(() => {
    // NOTE: loads *after* rendering, triggering waterfalls in children
    fetchNote(props.id).then(noteData => {
      setNote(noteData);
    });
  }, [props.id]);
  if (note == null) {
    return "Loading";
  } else {
    return (/* render note here... */);
  }
}

但是,当父组件和子组件都是此种情况时,在父组件完成加载其数据之前,子组件无法开始加载任何数据。但是,这种模式有一些有利方面。在各个组件中获取数据的一个好处是,它允许应用程序精确获取所需的数据,并避免为未呈现的 UI 部分获取数据。我们想找到一种避免客户端连续往返的方法,同时又避免过度提取不会使用的数据。

Server Components允许应用程序通过将串行请求移动到服务器来实现此目标。通过将此逻辑移至服务器,我们减少了请求延迟并提高了性能。更好的是,Server Components允许开发人员继续直接从其组件内部获取所需的最少数据:

// Note.server.js - Server Component

function Note(props) {
  // NOTE: loads *during* render, w low-latency data access on the server
  const note = db.notes.get(props.id);
  if (note == null) {
    // handle missing note
  }
  return (/* render note here... */);
}

# FAQ

# Server Components 会替代 SSR?

不,它们是互补的。SSR 主要是一种快速显示非响应的客户端组件的技术。在加载初始 HMTL 之后,您仍然需要下载,解析和执行这些客户端组件。

您可以将Server Components和 SSR 结合起来,其中Server Components首先渲染然后与客户端组件渲染一起注入到 html 中传递用户,已达到快速首屏的效果。当以这种方式组合在一起时,您仍然可以快速启动,但是您也可以大大减少需要在客户端上下载的 JS 数量。

# 这会取代GraphQL吗?

不会。GraphQL是一种构建API端点的方法,该方法支持跨语言边界的类型安全查询,并且可以帮助应用程序减少不足/过度获取并减少往返次数(以及其他功能)。相反,Server Components专注于构建用户界面。

# 这是否可以解决JavaScript中缺少编译器的问题?

不可以,静态编译器可以帮助解决某些问题,但是我们发现许多现实世界中的应用程序都有很多动态分支,例如用户特定的选项,A / B测试,功能标志等,这些都使静态优化达到极限.

参考react-rfc (opens new window)

【未经作者允许禁止转载】 Last Updated: 11/20/2021, 8:33:35 AM