The Forensics Of React Server Components (RSCs)
React 服务器组件 (RSC) 的取证

About The Author 关于作者

Lazar Nikolov is a Web Performance Developer Advocate at and instructor at egghead (@eggheadio). He is running, a community of curious … More about Lazar ↬
Lazar Nikolov 是 的 Web 性能开发倡导者和 Egghead (@eggheadio) 的讲师。他正在运营,这是一个充满好奇心的社区……更多关于 拉扎尔↬

Email Newsletter 电子邮件通讯

Weekly tips on front-end & UX.

Trusted by 200,000+ folks.
受到超过 200,000 人的信赖。

We love client-side rendering for the way it relieves the server of taxing operations, but serving an empty HTML page often leads to taxing user experiences during the initial page load. We love server-side rendering because it allows us to serve static assets on speedy CDNs, but they’re unfit for large-scale projects with dynamic content. React Server Components (RSCs) combine the best of both worlds, and author Lazar Nikolov thoroughly examines how we got here with a deep look at the impact that RSCs have on the page load timeline.
我们喜欢客户端渲染,因为它减轻了服务器繁重的操作,但提供空的 HTML 页面通常会导致初始页面加载期间的用户体验繁重。我们喜欢服务器端渲染,因为它允许我们在快速的 CDN 上提供静态资源,但它们不适合具有动态内容的大型项目。 React 服务器组件 (RSC) 结合了两全其美,作者 Lazar Nikolov 深入研究了 RSC 对页面加载时间线的影响,彻底研究了我们是如何做到这一点的。

In this article, we’re going to look deeply at React Server Components (RSCs). They are the latest innovation in React’s ecosystem, leveraging both server-side and client-side rendering as well as streaming HTML to deliver content as fast as possible.
在本文中,我们将深入研究 React 服务器组件(RSC)。它们是 React 生态系统中的最新创新,利用服务器端和客户端渲染以及流式 HTML 来尽可能快地交付内容。

We will get really nerdy to get a full understanding of how RFCs fit into the React picture, the level of control they offer over the rendering lifecycle of components, and what page loads look like with RFCs in place.
我们将非常书呆子般地全面了解 RFC 如何融入 React 图片、它们对组件渲染生命周期提供的控制级别,以及使用 RFC 时页面加载的样子。

But before we dive into all of that, I think it’s worth looking back at how React has rendered websites up until this point to set the context for why we need RFCs in the first place.
但在我们深入研究所有这些之前,我认为有必要回顾一下到目前为止 React 是如何渲染网站的,以便为我们首先需要 RFC 的原因设置背景。

The Early Days: React Client-Side Rendering #
早期:React 客户端渲染 #

The first React apps were rendered on the client side, i.e., in the browser. As developers, we wrote apps with JavaScript classes as components and packaged everything up using bundlers, like Webpack, in a nicely compiled and tree-shaken heap of code ready to ship in a production environment.
第一个 React 应用程序是在客户端(即浏览器)上呈现的。作为开发人员,我们使用 JavaScript 类作为组件编写应用程序,并使用 Webpack 等捆绑器将所有内容打包在经过良好编译和摇树优化的代码堆中,准备在生产环境中发布。

The HTML that returned from the server contained a few things, including:
从服务器返回的 HTML 包含一些内容,包括:

  • An HTML document with metadata in the <head> and a blank <div> in the <body> used as a hook to inject the app into the DOM;
    一个 HTML 文档, <head> 中包含元数据, <body> 中包含空白 <div> ,用作将应用程序注入 DOM 的钩子;
  • JavaScript resources containing React’s core code and the actual code for the web app, which would generate the user interface and populate the app inside of the empty <div>.
    JavaScript 资源包含 React 的核心代码和 Web 应用程序的实际代码,它将生成用户界面并填充空 <div> 内的应用程序。
Diagram of the client-side rendering process of a React app, starting with a blank loading page in the browser followed by a series of processes connected to CDNs and APIs to produce content on the loading page.
Figure 1. (Large preview)
图 1.(大预览)

A web app under this process is only fully interactive once JavaScript has fully completed its operations. You can probably already see the tension here that comes with an improved developer experience (DX) that negatively impacts the user experience (UX).
只有当 JavaScript 完全完成其操作后,此流程下的 Web 应用程序才能完全交互。您可能已经看到了改进的开发人员体验 (DX) 带来的紧张局势,这对用户体验 (UX) 产生了负面影响。

The truth is that there were (and are) pros and cons to CSR in React. Looking at the positives, web applications delivered smooth, quick transitions that reduced the overall time it took to load a page, thanks to reactive components that update with user interactions without triggering page refreshes. CSR lightens the server load and allows us to serve assets from speedy content delivery networks (CDNs) capable of delivering content to users from a server location geographically closer to the user for even more optimized page loads.
事实是,React 中的 CSR 有利有弊。从积极的方面来看,Web 应用程序提供了平滑、快速的转换,减少了加载页面所需的总时间,这要归功于反应式组件,它们可以随着用户交互而更新,而不会触发页面刷新。 CSR 减轻了服务器负载,使我们能够通过快速内容交付网络 (CDN) 提供资产,该网络能够从地理位置更靠近用户的服务器位置向用户交付内容,从而实现更优化的页面加载。

There are also not-so-great consequences that come with CSR, most notably perhaps that components could fetch data independently, leading to waterfall network requests that dramatically slow things down. This may sound like a minor nuisance on the UX side of things, but the damage can actually be quite large on a human level. Eric Bailey’s “Modern Health, frameworks, performance, and harm” should be a cautionary tale for all CSR work.
CSR 也带来了不太严重的后果,最值得注意的是组件可以独立获取数据,从而导致瀑布式网络请求,从而大大降低速度。这在用户体验方面听起来可能是一个小麻烦,但实际上对人类造成的损害可能相当大。埃里克·贝利(Eric Bailey)的《现代健康、框架、绩效和危害》应该成为所有企业社会责任工作的警示。

Other negative CSR consequences are not quite as severe but still lead to damage. For example, it used to be that an HTML document containing nothing but metadata and an empty <div> was illegible to search engine crawlers that never get the fully-rendered experience. While that’s solved today, the SEO hit at the time was an anchor on company sites that rely on search engine traffic to generate revenue.
其他负面的企业社会责任后果虽然没有那么严重,但仍然会造成损害。例如,过去,仅包含元数据和空 <div> 的HTML文档对于永远无法获得完全渲染体验的搜索引擎爬虫来说是难以辨认的。虽然这个问题今天已经解决,但当时的 SEO 热门是依靠搜索引擎流量来产生收入的公司网站的锚点。

The Shift: Server-Side Rendering (SSR) #
转变:服务器端渲染 (SSR) #

Something needed to change. CSR presented developers with a powerful new approach for constructing speedy, interactive interfaces, but users everywhere were inundated with blank screens and loading indicators to get there. The solution was to move the rendering experience from the client to the server. I know it sounds funny that we needed to improve something by going back to the way it was before.
有些事情需要改变。 CSR 为开发人员提供了一种强大的新方法来构建快速的交互式界面,但世界各地的用户都被空白屏幕和加载指示器所淹没。解决方案是将渲染体验从客户端转移到服务器。我知道我们需要通过回到以前的方式来改进某些东西,这听起来很有趣。

So, yes, React gained server-side rendering (SSR) capabilities. At one point, SSR was such a topic in the React community that it had a moment in the spotlight. The move to SSR brought significant changes to app development, specifically in how it influenced React behavior and how content could be delivered by way of servers instead of browsers.
所以,是的,React 获得了服务器端渲染(SSR)功能。曾经,SSR 是 React 社区中的一个热门话题,一度成为人们关注的焦点。转向 SSR 给应用程序开发带来了重大变化,特别是它如何影响 React 行为以及如何通过服务器而不是浏览器传递内容。

Diagram of the server-side rendering process of a React app, starting with a blank loading page in the browser followed by a screen of un-interactive content, then a fully interactive page of content.
Figure 2. (Large preview)
图 2.(大预览)

Addressing CSR Limitations #

Instead of sending a blank HTML document with SSR, we rendered the initial HTML on the server and sent it to the browser. The browser was able to immediately start displaying the content without needing to show a loading indicator. This significantly improves the First Contentful Paint (FCP) performance metric in Web Vitals.
我们没有使用 SSR 发送空白 HTML 文档,而是在服务器上渲染初始 HTML 并将其发送到浏览器。浏览器能够立即开始显示内容,而无需显示加载指示器。这显着提高了 Web Vitals 中的首次内容绘制 (FCP) 性能指标。

Server-side rendering also fixed the SEO issues that came with CSR. Since the crawlers received the content of our websites directly, they were then able to index it right away. The data fetching that happens initially also takes place on the server, which is a plus because it’s closer to the data source and can eliminate fetch waterfalls if done properly.
服务器端渲染还解决了 CSR 带来的 SEO 问题。由于爬虫直接接收我们网站的内容,因此他们能够立即对其进行索引。最初发生的数据获取也发生在服务器上,这是一个优点,因为它更接近数据源,并且如果做得正确,可以消除获取瀑布。

Hydration # 保湿#

SSR has its own complexities. For React to make the static HTML received from the server interactive, it needs to hydrate it. Hydration is the process that happens when React reconstructs its Virtual Document Object Model (DOM) on the client side based on what was in the DOM of the initial HTML.
SSR 有其自身的复杂性。为了使从服务器接收到的静态 HTML 具有交互性,React 需要对其进行水化。 Hydration 是 React 根据初始 HTML DOM 中的内容在客户端重建其虚拟文档对象模型 (DOM) 时发生的过程。

Note: React maintains its own Virtual DOM because it’s faster to figure out updates on it instead of the actual DOM. It synchronizes the actual DOM with the Virtual DOM when it needs to update the UI but performs the diffing algorithm on the Virtual DOM.
注意:React 维护自己的虚拟 DOM,因为它比实际 DOM 更快地计算出更新。当需要更新 UI 时,它会将实际 DOM 与虚拟 DOM 同步,但会在虚拟 DOM 上执行比较算法。

We now have two flavors of Reacts:
我们现在有两种风格的 React:

  1. A server-side flavor that knows how to render static HTML from our component tree,
    服务器端风格,知道如何从我们的组件树渲染静态 HTML,
  2. A client-side flavor that knows how to make the page interactive.

We’re still shipping React and code for the app to the browser because — in order to hydrate the initial HTML — React needs the same components on the client side that were used on the server. During hydration, React performs a process called reconciliation in which it compares the server-rendered DOM with the client-rendered DOM and tries to identify differences between the two. If there are differences between the two DOMs, React attempts to fix them by rehydrating the component tree and updating the component hierarchy to match the server-rendered structure. And if there are still inconsistencies that cannot be resolved, React will throw errors to indicate the problem. This problem is commonly known as a hydration error.
我们仍然将 React 和应用程序代码发送到浏览器,因为为了使初始 HTML 水合,React 需要在客户端使用与服务器上使用的相同的组件。在水合过程中,React 执行一个称为协调的过程,在该过程中,它将服务器渲染的 DOM 与客户端渲染的 DOM 进行比较,并尝试识别两者之间的差异。如果两个 DOM 之间存在差异,React 会尝试通过重新调整组件树并更新组件层次结构以匹配服务器渲染的结构来修复它们。如果仍然存在无法解决的不一致,React 将抛出错误来指示问题。这个问题通常被称为水合错误。

SSR Drawbacks # SSR 缺点 #

SSR is not a silver bullet solution that addresses CSR limitations. SSR comes with its own drawbacks. Since we moved the initial HTML rendering and data fetching to the server, those servers are now experiencing a much greater load than when we loaded everything on the client.
SSR 并不是解决 CSR 限制的灵丹妙药。 SSR 有其自身的缺点。由于我们将初始 HTML 渲染和数据获取移至服务器,这些服务器现在承受的负载比我们在客户端加载所有内容时要大得多。

Remember when I mentioned that SSR generally improves the FCP performance metric? That may be true, but the Time to First Byte (TTFB) performance metric took a negative hit with SSR. The browser literally has to wait for the server to fetch the data it needs, generate the initial HTML, and send the first byte. And while TTFB is not a Core Web Vital metric in itself, it influences the metrics. A negative TTFB leads to negative Core Web Vitals metrics.
还记得我提到过 SSR 通常会提高 FCP 性能指标吗?这可能是真的,但第一个字节的时间 (TTFB) 性能指标受到 SSR 的负面影响。浏览器实际上必须等待服务器获取所需的数据、生成初始 HTML 并发送第一个字节。虽然 TTFB 本身不是核心 Web 重要指标,但它会影响指标。负的 TTFB 会导致负的 Core Web Vitals 指标。

Another drawback of SSR is that the entire page is unresponsive until client-side React has finished hydrating it. Interactive elements cannot listen and “react” to user interactions before React hydrates them, i.e., React attaches the intended event listeners to them. The hydration process is typically fast, but the internet connection and hardware capabilities of the device in use can slow down rendering by a noticeable amount.
SSR 的另一个缺点是整个页面没有响应,直到客户端 React 完成水化。在 React 水合交互元素之前,交互元素无法监听用户交互并对其进行“反应”,即 React 将预期的事件监听器附加到它们上。水合过程通常很快,但所用设备的互联网连接和硬件功能可能会显着减慢渲染速度。

The Present: A Hybrid Approach #

So far, we have covered two different flavors of React rendering: CSR and SSR. While the two were attempts to improve one another, we now get the best of both worlds, so to speak, as SSR has branched into three additional React flavors that offer a hybrid approach in hopes of reducing the limitations that come with CSR and SSR.
到目前为止,我们已经介绍了两种不同风格的 React 渲染:CSR 和 SSR。虽然两者都试图相互改进,但可以说,我们现在两全其美,因为 SSR 已经分支为另外三种 React 风格,它们提供了一种混合方法,希望减少 CSR 和 SSR 带来的限制。

We’ll look at the first two — static site generation and incremental static regeneration — before jumping into an entire discussion on React Server Components, the third flavor.
我们将先看看前两个——静态站点生成和增量静态重新生成——然后再进入关于第三种类型的 React Server Components 的完整讨论。

Static Site Generation (SSG) #
静态站点生成 (SSG) #

Instead of regenerating the same HTML code on every request, we came up with SSG. This React flavor compiles and builds the entire app at build time, generating static (as in vanilla HTML and CSS) files that are, in turn, hosted on a speedy CDN.
我们没有在每个请求上重新生成相同的 HTML 代码,而是提出了 SSG。这种 React 风格在构建时编译和构建整个应用程序,生成静态(如普通 HTML 和 CSS)文件,这些文件又托管在快速 CDN 上。

As you might suspect, this hybrid approach to rendering is a nice fit for smaller projects where the content doesn’t change much, like a marketing site or a personal blog, as opposed to larger projects where content may change with user interactions, like an e-commerce site.

SSG reduces the burden on the server while improving performance metrics related to TTFB because the server no longer has to perform heavy, expensive tasks for re-rendering the page.
SSG 减轻了服务器的负担,同时提高了与 TTFB 相关的性能指标,因为服务器不再需要执行繁重、昂贵的任务来重新呈现页面。

Incremental Static Regeneration (ISR) #
增量静态再生 (ISR) #

One SSG drawback is having to rebuild all of the app’s code when a content change is needed. The content is set in stone — being static and all — and there’s no way to change just one part of it without rebuilding the whole thing.
SSG 的一个缺点是,当需要更改内容时,必须重建所有应用程序的代码。内容是一成不变的——都是静态的——并且没有办法在不重建整个内容的情况下只改变其中的一部分。

The Next.js team created the second hybrid flavor of React that addresses the drawback of complete SSG rebuilds: incremental static regeneration (ISR). The name says a lot about the approach in that ISR only rebuilds what’s needed instead of the entire thing. We generate the “initial version” of the page statically during build time but are also able to rebuild any page containing stale data after a user lands on it (i.e., the server request triggers the data check).
Next.js 团队创建了 React 的第二种混合风格,解决了完整 SSG 重建的缺点:增量静态再生 (ISR)。这个名字充分说明了这种方法,ISR 只重建需要的东西,而不是整个东西。我们在构建时静态生成页面的“初始版本”,但也能够在用户登陆后重建任何包含陈旧数据的页面(即服务器请求触发数据检查)。

From that point on, the server will serve new versions of that page statically in increments when needed. That makes ISR a hybrid approach that is neatly positioned between SSG and traditional SSR.
从那时起,服务器将在需要时以增量方式静态地提供该页面的新版本。这使得 ISR 成为一种介于 SSG 和传统 SSR 之间的混合方法。

At the same time, ISR does not address the “stale content” symptom, where users may visit a page before it has finished being generated. Unlike SSG, ISR needs an actual server to regenerate individual pages in response to a user’s browser making a server request. That means we lose the valuable ability to deploy ISR-based apps on a CDN for optimized asset delivery.
同时,ISR 没有解决“陈旧内容”症状,即用户可能会在页面生成完成之前访问该页面。与 SSG 不同,ISR 需要一个实际的服务器来重新生成各个页面,以响应用户浏览器发出的服务器请求。这意味着我们失去了在 CDN 上部署基于 ISR 的应用程序以优化资产交付的宝贵能力。

The Future: React Server Components #
未来:React 服务器组件 #

Up until this point, we’ve juggled between CSR, SSR, SSG, and ISR approaches, where all make some sort of trade-off, negatively affecting performance, development complexity, and user experience. Newly introduced React Server Components (RSC) aim to address most of these drawbacks by allowing us — the developer — to choose the right rendering strategy for each individual React component.
到目前为止,我们一直在 CSR、SSR、SSG 和 ISR 方法之间进行权衡,所有这些方法都会做出某种权衡,对性能、开发复杂性和用户体验产生负面影响。新推出的 React Server Components (RSC) 旨在通过允许我们(开发人员)为每个单独的 React 组件选择正确的渲染策略来解决大多数这些缺点。

RSCs can significantly reduce the amount of JavaScript shipped to the client since we can selectively decide which ones to serve statically on the server and which render on the client side. There’s a lot more control and flexibility for striking the right balance for your particular project.
RSC 可以显着减少发送到客户端的 JavaScript 数量,因为我们可以有选择地决定哪些在服务器上静态提供,哪些在客户端呈现。有更多的控制和灵活性来为您的特定项目取得适当的平衡。

Note: It’s important to keep in mind that as we adopt more advanced architectures, like RSCs, monitoring solutions become invaluable. Sentry offers robust performance monitoring and error-tracking capabilities that help you keep an eye on the real-world performance of your RSC-powered application. Sentry also helps you gain insights into how your releases are performing and how stable they are, which is yet another crucial feature to have while migrating your existing applications to RSCs. Implementing Sentry in an RSC-enabled framework like Next.js is as easy as running a single terminal command.
注意:重要的是要记住,随着我们采用更先进的架构(例如 RSC),监控解决方案变得非常宝贵。 Sentry 提供强大的性能监控和错误跟踪功能,帮助您关注 RSC 支持的应用程序的实际性能。 Sentry 还可以帮助您深入了解版本的执行情况及其稳定性,这是将现有应用程序迁移到 RSC 时的另一个重要功能。在支持 RSC 的框架(如 Next.js)中实现 Sentry 就像运行单个终端命令一样简单。

But what exactly is an RSC? Let’s pick one apart to see how it works under the hood.
但 RSC 到底是什么?让我们拆开其中一个来看看它是如何工作的。

The Anatomy of React Server Components #
React 服务器组件的剖析 #

This new approach introduces two types of rendering components: Server Components and Client Components. The differences between these two are not how they function but where they execute and the environments they’re designed for. At the time of this writing, the only way to use RSCs is through React frameworks. And at the moment, there are only three frameworks that support them: Next.js, Gatsby, and RedwoodJS.
这种新方法引入了两种类型的渲染组件:服务器组件和客户端组件。两者之间的区别不在于它们的功能,而在于它们的执行位置和设计环境。在撰写本文时,使用 RSC 的唯一方法是通过 React 框架。目前,只有三个框架支持它们:Next.js、Gatsby 和 RedwoodJS。

Wire diagram showing connected server components and client components represented as gray and blue dots, respectively.
Figure 3: Example of an architecture consisting of Server Components and Client Components. (Large preview)
图 3:由服务器组件和客户端组件组成的架构示例。 (大预览)

Server Components # 服务器组件#

Server Components are designed to be executed on the server, and their code is never shipped to the browser. The HTML output and any props they might be accepting are the only pieces that are served. This approach has multiple performance benefits and user experience enhancements:
服务器组件被设计为在服务器上执行,它们的代码永远不会传送到浏览器。 HTML 输出和它们可能接受的任何 props 是唯一提供的部分。这种方法具有多种性能优势和用户体验增强:

  • Server Components allow for large dependencies to remain on the server side.

    Imagine using a large library for a component. If you’re executing the component on the client side, it means that you’re also shipping the full library to the browser. With Server Components, you’re only taking the static HTML output and avoiding having to ship any JavaScript to the browser. Server Components are truly static, and they remove the whole hydration step.
    想象一下为一个组件使用一个大型库。如果您在客户端执行该组件,则意味着您还将完整的库传送到浏览器。使用服务器组件,您只需获取静态 HTML 输出,而无需将任何 JavaScript 传送到浏览器。服务器组件是真正静态的,它们消除了整个水合步骤。
  • Server Components are located much closer to the data sources — e.g., databases or file systems — they need to generate code.

    They also leverage the server’s computational power to speed up compute-intensive rendering tasks and send only the generated results back to the client. They are also generated in a single pass, which avoids request waterfalls and HTTP round trips.
    他们还利用服务器的计算能力来加速计算密集型渲染任务,并仅将生成的结果发送回客户端。它们也是在单次传递中生成的,这避免了请求瀑布和 HTTP 往返。
  • Server Components safely keep sensitive data and logic away from the browser.

    That’s thanks to the fact that personal tokens and API keys are executed on a secure server rather than the client.
    这要归功于个人令牌和 API 密钥是在安全服务器而不是客户端上执行的。
  • The rendering results can be cached and reused between subsequent requests and even across different sessions.

    This significantly reduces rendering time, as well as the overall amount of data that is fetched for each request.

This architecture also makes use of HTML streaming, which means the server defers generating HTML for specific components and instead renders a fallback element in their place while it works on sending back the generated HTML. Streaming Server Components wrap components in <Suspense> tags that provide a fallback value. The implementing framework uses the fallback initially but streams the newly generated content when it‘s ready. We’ll talk more about streaming, but let’s first look at Client Components and compare them to Server Components.
此架构还利用 HTML 流,这意味着服务器推迟为特定组件生成 HTML,而是在其位置上呈现后备元素,同时发送回生成的 HTML。流服务器组件将组件包装在提供后备值的 <Suspense> 标记中。实施框架最初使用回退,但在准备好时流式传输新生成的内容。我们将更多地讨论流媒体,但让我们首先看看客户端组件并将它们与服务器组件进行比较。

Client Components # 客户端组件#

Client Components are the components we already know and love. They’re executed on the client side. Because of this, Client Components are capable of handling user interactions and have access to the browser APIs like localStorage and geolocation.
客户端组件是我们已经了解和喜爱的组件。它们在客户端执行。因此,客户端组件能够处理用户交互并可以访问浏览器 API,例如 localStorage 和地理位置。

The term “Client Component” doesn’t describe anything new; they merely are given the label to help distinguish the “old” CSR components from Server Components. Client Components are defined by a "use client" directive at the top of their files.
术语“客户端组件”并没有描述任何新东西;它们只是被赋予标签来帮助区分“旧”CSR 组件和服务器组件。客户端组件由其文件顶部的 "use client" 指令定义。

"use client"
export default function LikeButton() {
  const likePost = () => {
    // ...
  return (
    <button onClick={likePost}>Like</button>

In Next.js, all components are Server Components by default. That’s why we need to explicitly define our Client Components with "use client". There’s also a "use server" directive, but it’s used for Server Actions (which are RPC-like actions that invoked from the client, but executed on the server). You don’t use it to define your Server Components.
在 Next.js 中,所有组件默认都是服务器组件。这就是为什么我们需要使用 "use client" 显式定义我们的客户端组件。还有一个 "use server" 指令,但它用于服务器操作(这是从客户端调用但在服务器上执行的类似 RPC 的操作)。您不使用它来定义您的服务器组件。

You might (rightfully) assume that Client Components are only rendered on the client, but Next.js renders Client Components on the server to generate the initial HTML. As a result, browsers can immediately start rendering them and then perform hydration later.
您可能(正确地)假设客户端组件仅在客户端上呈现,但 Next.js 在服务器上呈现客户端组件以生成初始 HTML。因此,浏览器可以立即开始渲染它们,然后再执行水合作用。

The Relationship Between Server Components and Client Components #
服务器组件和客户端组件之间的关系 #

Client Components can only explicitly import other Client Components. In other words, we’re unable to import a Server Component into a Client Component because of re-rendering issues. But we can have Server Components in a Client Component’s subtree — only passed through the children prop. Since Client Components live in the browser and they handle user interactions or define their own state, they get to re-render often. When a Client Component re-renders, so will its subtree. But if its subtree contains Server Components, how would they re-render? They don’t live on the client side. That’s why the React team put that limitation in place.
客户端组件只能显式导入其他客户端组件。换句话说,由于重新渲染问题,我们无法将服务器组件导入客户端组件。但是我们可以在客户端组件的子树中拥有服务器组件——仅通过 children 属性传递。由于客户端组件位于浏览器中并且它们处理用户交互或定义自己的状态,因此它们经常重新渲染。当客户端组件重新呈现时,其子树也会重新呈现。但如果它的子树包含服务器组件,它们将如何重新渲染?他们并不住在客户端。这就是 React 团队设置该限制的原因。

But hold on! We actually can import Server Components into Client Components. It’s just not a direct one-to-one relationship because the Server Component will be converted into a Client Component. If you’re using server APIs that you can’t use in the browser, you’ll get an error; if not — you’ll have a Server Component whose code gets “leaked” to the browser.
但坚持住!我们实际上可以将服务器组件导入到客户端组件中。它只是不是直接的一对一关系,因为服务器组件将转换为客户端组件。如果您使用的服务器 API 无法在浏览器中使用,则会出现错误;如果没有,您将拥有一个服务器组件,其代码会“泄漏”到浏览器。

This is an incredibly important nuance to keep in mind as you work with RSCs.
当您与 RSC 合作时,请记住这一点非常重要的细微差别。

The Rendering Lifecycle #

Here’s the order of operations that Next.js takes to stream contents:
以下是 Next.js 流式传输内容的操作顺序:

  1. The app router matches the page’s URL to a Server Component, builds the component tree, and instructs the server-side React to render that Server Component and all of its children components.
    应用程序路由器将页面的 URL 与服务器组件相匹配,构建组件树,并指示服务器端 React 渲染该服务器组件及其所有子组件。
  2. During render, React generates an “RSC Payload”. The RSC Payload informs Next.js about the page and what to expect in return, as well as what to fall back to during a <Suspense>.
    在渲染期间,React 生成一个“RSC Payload”。 RSC 有效负载通知 Next.js 有关该页面的信息以及期望返回的内容,以及在 <Suspense> 期间回退到的内容。
  3. If React encounters a suspended component, it pauses rendering that subtree and uses the suspended component’s fallback value.
    如果 React 遇到挂起的组件,它会暂停渲染该子树并使用挂起的组件的后备值。
  4. When React loops through the last static component, Next.js prepares the generated HTML and the RSC Payload before streaming it back to the client through one or multiple chunks.
    当 React 循环最后一个静态组件时,Next.js 会准备生成的 HTML 和 RSC 有效负载,然后通过一个或多个块将其流回客户端。
  5. The client-side React then uses the instructions it has for the RSC Payload and client-side components to render the UI. It also hydrates each Client Component as they load.
    然后,客户端 React 使用 RSC 有效负载和客户端组件的指令来呈现 UI。它还会在加载每个客户端组件时对其进行水合。
  6. The server streams in the suspended Server Components as they become available as an RSC Payload. Children of Client Components are also hydrated at this time if the suspended component contains any.
    当暂停的服务器组件可用作 RSC 有效负载时,服务器会在其中进行流式传输。如果悬浮组件含有水合成分,则客户端组件的子级此时也会水合。

We will look at the RSC rendering lifecycle from the browser’s perspective momentarily. For now, the following figure illustrates the outlined steps we covered.
我们将从浏览器的角度来看看 RSC 渲染生命周期。目前,下图说明了我们所介绍的概述步骤。

Wire diagram of the RSC rendering lifecycle going from a blank page to a page shell to a complete page.
Figure 4: Diagram of the RSC Rendering Lifecycle. (Large preview)
图 4:RSC 渲染生命周期图。 (大预览)

We’ll see this operation flow from the browser’s perspective in just a bit.

RSC Payload # RSC 有效负载#

The RSC payload is a special data format that the server generates as it renders the component tree, and it includes the following:
RSC 有效负载是服务器在渲染组件树时生成的一种特殊数据格式,它包括以下内容:

  • The rendered HTML, 渲染的 HTML,
  • Placeholders where the Client Components should be rendered,
  • References to the Client Components’ JavaScript files,
    对客户端组件的 JavaScript 文件的引用,
  • Instructions on which JavaScript files it should invoke,
    有关应调用哪些 JavaScript 文件的说明,
  • Any props passed from a Server Component to a Client Component.

There’s no reason to worry much about the RSC payload, but it’s worth understanding what exactly the RSC payload contains. Let’s examine an example (truncated for brevity) from a demo app I created:
没有理由太担心 RSC 有效负载,但值得了解 RSC 有效负载到底包含什么。让我们看一下我创建的演示应用程序中的示例(为简洁起见,已截断):

7:["$","main",null,{"className":"page_main__GlU4n","children":[["$","$Lf",null,{}],["$","$8",null,{"fallback":["$","p",null,{"children":"🌀 loading products..."}],"children":"$L10"}]]}]
c:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]...
9:["$","p",null,{"children":["🛍️ ",3]}]
10:["$","ul",null,{"children":[["$","li","1",{"children":["Gloves"," - $",20,["$...

To find this code in the demo app, open your browser’s developer tools at the Elements tab and look at the <script> tags at the bottom of the page. They’ll contain lines like:
要在演示应用程序中找到此代码,请在“元素”选项卡中打开浏览器的开发人员工具,然后查看页面底部的 <script> 标签。它们将包含如下行:


Every line from the snippet above is an individual RSC payload. You can see that each line starts with a number or a letter, followed by a colon, and then an array that’s sometimes prefixed with letters. We won’t get into too deep in detail as to what they mean, but in general:
上面代码片段中的每一行都是一个单独的 RSC 负载。您可以看到每一行都以数字或字母开头,后跟冒号,然后是有时以字母为前缀的数组。我们不会太深入地了解它们的含义,但总的来说:

  • HL payloads are called “hints” and link to specific resources like CSS and fonts.
    HL 有效负载称为“提示”,链接到 CSS 和字体等特定资源。
  • I payloads are called “modules,” and they invoke specific scripts. This is how Client Components are being loaded as well. If the Client Component is part of the main bundle, it’ll execute. If it’s not (meaning it’s lazy-loaded), a fetcher script is added to the main bundle that fetches the component’s CSS and JavaScript files when it needs to be rendered. There’s going to be an I payload sent from the server that invokes the fetcher script when needed.
    I 有效负载称为“模块”,它们调用特定的脚本。这也是加载客户端组件的方式。如果客户端组件是主包的一部分,它将执行。如果不是(意味着它是延迟加载的),则会将一个获取器脚本添加到主包中,以便在需要渲染时获取组件的 CSS 和 JavaScript 文件。服务器会发送一个 I 有效负载,在需要时调用 fetcher 脚本。
  • "$" payloads are DOM definitions generated for a certain Server Component. They are usually accompanied by actual static HTML streamed from the server. That’s what happens when a suspended component becomes ready to be rendered: the server generates its static HTML and RSC Payload and then streams both to the browser.
    "$" 有效负载是为某个服务器组件生成的 DOM 定义。它们通常伴随着从服务器流式传输的实际静态 HTML。这就是当挂起的组件准备好渲染时会发生的情况:服务器生成其静态 HTML 和 RSC 有效负载,然后将两者传输到浏览器。

Streaming # 流媒体 #

Streaming allows us to progressively render the UI from the server. With RSCs, each component is capable of fetching its own data. Some components are fully static and ready to be sent immediately to the client, while others require more work before loading. Based on this, Next.js splits that work into multiple chunks and streams them to the browser as they become ready. So, when a user visits a page, the server invokes all Server Components, generates the initial HTML for the page (i.e., the page shell), replaces the “suspended” components’ contents with their fallbacks, and streams all of that through one or multiple chunks back to the client.
流式传输允许我们从服务器逐步渲染 UI。通过 RSC,每个组件都能够获取自己的数据。有些组件是完全静态的,可以立即发送到客户端,而其他组件则需要在加载之前进行更多工作。基于此,Next.js 将工作拆分为多个块,并在准备就绪时将它们流式传输到浏览器。因此,当用户访问页面时,服务器调用所有服务器组件,生成页面的初始 HTML(即页面 ​​shell),用其后备替换“挂起”组件的内容,并通过一个流式传输所有内容。或多个块返回给客户端。

The server returns a Transfer-Encoding: chunked header that lets the browser know to expect streaming HTML. This prepares the browser for receiving multiple chunks of the document, rendering them as it receives them. We can actually see the header when opening Developer Tools at the Network tab. Trigger a refresh and click on the document request.
服务器返回一个 Transfer-Encoding: chunked 标头,让浏览器知道需要流式 HTML。这使浏览器准备好接收文档的多个块,并在接收到它们时呈现它们。在“网络”选项卡中打开“开发人员工具”时,我们实际上可以看到标题。触发刷新并单击文档请求。

Response header output highlighting the line containing the chunked transfer endcoding
Figure 5: Providing a hint to the browser to expect HTML streaming. (Large preview)
图 5:向浏览器提供预期 HTML 流的提示。 (大预览)

We can also debug the way Next.js sends the chunks in a terminal with the curl command:
我们还可以使用 curl 命令调试 Next.js 在终端中发送块的方式:

curl -D - --raw localhost:3000 > chunked-response.txt
Headers and chunked HTML payloads.
Figure 6. (Large preview)
图 6.(大预览)

You probably see the pattern. For each chunk, the server responds with the chunk’s size before sending the chunk’s contents. Looking at the output, we can see that the server streamed the entire page in 16 different chunks. At the end, the server sends back a zero-sized chunk, indicating the end of the stream.
您可能会看到这种模式。对于每个块,服务器在发送块的内容之前响应块的大小。查看输出,我们可以看到服务器以 16 个不同的块传输整个页面。最后,服务器发回一个零大小的块,指示流的结束。

The first chunk starts with the <!DOCTYPE html> declaration. The second-to-last chunk, meanwhile, contains the closing </body> and </html> tags. So, we can see that the server streams the entire document from top to bottom, then pauses to wait for the suspended components, and finally, at the end, closes the body and HTML before it stops streaming.
第一个块以 <!DOCTYPE html> 声明开头。同时,倒数第二个块包含结束 </body></html> 标签。因此,我们可以看到服务器从上到下流式传输整个文档,然后暂停以等待挂起的组件,最后在停止流式传输之前关闭正文和 HTML。

Even though the server hasn’t completely finished streaming the document, the browser’s fault tolerance features allow it to draw and invoke whatever it has at the moment without waiting for the closing </body> and </html> tags.
即使服务器尚未完全完成文档的流式传输,浏览器的容错功能也允许它绘制和调用当前拥有的任何内容,而无需等待结束 </body></html> 标记。

Suspending Components # 挂起组件#

We learned from the render lifecycle that when a page is visited, Next.js matches the RSC component for that page and asks React to render its subtree in HTML. When React stumbles upon a suspended component (i.e., async function component), it grabs its fallback value from the <Suspense> component (or the loading.js file if it’s a Next.js route), renders that instead, then continues loading the other components. Meanwhile, the RSC invokes the async component in the background, which is streamed later as it finishes loading.
我们从渲染生命周期了解到,当访问页面时,Next.js 会匹配该页面的 RSC 组件,并要求 React 以 HTML 形式渲染其子树。当 React 偶然发现一个挂起的组件(即异步函数组件)时,它会从 <Suspense> 组件(如果是 Next.js 路由,则从 loading.js 文件)获取其后备值,渲染该组件,然后继续加载其他组件。同时,RSC 在后台调用异步组件,该组件稍后在完成加载时进行流式传输。

At this point, Next.js has returned a full page of static HTML that includes either the components themselves (rendered in static HTML) or their fallback values (if they’re suspended). It takes the static HTML and RSC payload and streams them back to the browser through one or multiple chunks.
此时,Next.js 已返回完整的静态 HTML 页面,其中包括组件本身(以静态 HTML 呈现)或它们的后备值(如果它们被挂起)。它获取静态 HTML 和 RSC 有效负载,并通过一个或多个块将它们流式传输回浏览器。

Showing suspended component fallbacks
Figure 7. (Large preview)
图 7.(大预览)

As the suspended components finish loading, React generates HTML recursively while looking for other nested <Suspense> boundaries, generates their RSC payloads and then lets Next.js stream the HTML and RSC Payload back to the browser as new chunks. When the browser receives the new chunks, it has the HTML and RSC payload it needs and is ready to replace the fallback element from the DOM with the newly-streamed HTML. And so on.
当挂起的组件完成加载时,React 会递归生成 HTML,同时查找其他嵌套的 <Suspense> 边界,生成其 RSC 有效负载,然后让 Next.js 将 HTML 和 RSC 有效负载作为新块流式传输回浏览器。当浏览器收到新块时,它具有所需的 HTML 和 RSC 有效负载,并准备好用新流式传输的 HTML 替换 DOM 中的后备元素。等等。

Static HTML and RSC Payload replacing suspended fallback values.
Figure 8. (Large preview)
图 8.(大预览)

In Figures 7 and 8, notice how the fallback elements have a unique ID in the form of B:0, B:1, and so on, while the actual components have a similar ID in a similar form: S:0 and S:1, and so on.
在图 7 和 8 中,请注意后备元素如何具有 B:0B:1 等形式的唯一 ID,而实际组件具有类似形式的相似 ID: S:0 和 @3 # , 等等。

Along with the first chunk that contains a suspended component’s HTML, the server also ships an $RC function (i.e., completeBoundary from React’s source code) that knows how to find the B:0 fallback element in the DOM and replace it with the S:0 template it received from the server. That’s the “replacer” function that lets us see the component contents when they arrive in the browser.
除了包含挂起组件 HTML 的第一个块之外,服务器还提供了一个 $RC 函数(即 React 源代码中的 completeBoundary ),该函数知道如何在 DOM 中查找 B:0 后备元素并将其替换为从服务器收到的 S:0 模板。这就是“替换器”功能,让我们可以在组件内容到达浏览器时看到它们。

The entire page eventually finishes loading, chunk by chunk.

Lazy-Loading Components #
延迟加载组件 #

If a suspended Server Component contains a lazy-loaded Client Component, Next.js will also send an RSC payload chunk containing instructions on how to fetch and load the lazy-loaded component’s code. This represents a significant performance improvement because the page load isn’t dragged out by JavaScript, which might not even be loaded during that session.
如果挂起的服务器组件包含延迟加载的客户端组件,Next.js 还将发送一个 RSC 有效负载块,其中包含有关如何获取和加载延迟加载组件的代码的说明。这代表了显着的性能改进,因为页面加载不会被 JavaScript 拖出,甚至在该会话期间可能不会加载。

Fetching additional JavaScript and CSS files for a lazy-loaded Client Component, as shown in developer tools.
Figure 9. (Large preview)
图 9.(大预览)

At the time I’m writing this, the dynamic method to lazy-load a Client Component in a Server Component in Next.js does not work as you might expect. To effectively lazy-load a Client Component, put it in a “wrapper” Client Component that uses the dynamic method itself to lazy-load the actual Client Component. The wrapper will be turned into a script that fetches and loads the Client Component’s JavaScript and CSS files at the time they’re needed.
在我写这篇文章时,在 Next.js 的服务器组件中延迟加载客户端组件的动态方法并没有像您预期的那样工作。要有效地延迟加载客户端组件,请将其放入“包装器”客户端组件中,该组件使用 dynamic 方法本身来延迟加载实际的客户端组件。包装器将变成一个脚本,在需要时获取并加载客户端组件的 JavaScript 和 CSS 文件。

TL;DR # 长话短说#

I know that’s a lot of plates spinning and pieces moving around at various times. What it boils down to, however, is that a page visit triggers Next.js to render as much HTML as it can, using the fallback values for any suspended components, and then sends that to the browser. Meanwhile, Next.js triggers the suspended async components and gets them formatted in HTML and contained in RSC Payloads that are streamed to the browser, one by one, along with an $RC script that knows how to swap things out.
我知道有很多盘子在旋转,碎片在不同的时间移动。然而,归根结底,页面访问会触发 Next.js 使用任何挂起组件的回退值来渲染尽可能多的 HTML,然后将其发送到浏览器。同时,Next.js 触发挂起的异步组件,并将它们格式化为 HTML 格式并包含在 RSC 有效负载中,这些有效负载连同知道如何交换内容的 $RC 脚本一一流式传输到浏览器。

The Page Load Timeline #
页面加载时间线 #

By now, we should have a solid understanding of how RSCs work, how Next.js handles their rendering, and how all the pieces fit together. In this section, we’ll zoom in on what exactly happens when we visit an RSC page in the browser.
到现在为止,我们应该对 RSC 的工作原理、Next.js 如何处理其渲染以及所有部分如何组合在一起有了深入的了解。在本节中,我们将重点介绍当我们在浏览器中访问 RSC 页面时到底发生了什么。

The Initial Load # 初始负载#

As we mentioned in the TL;DR section above, when visiting a page, Next.js will render the initial HTML minus the suspended component and stream it to the browser as part of the first streaming chunks.
正如我们在上面的 TL;DR 部分中提到的,当访问页面时,Next.js 将渲染减去挂起组件的初始 HTML,并将其作为第一个流块的一部分流式传输到浏览器。

To see everything that happens during the page load, we’ll visit the “Performance” tab in Chrome DevTools and click on the “reload” button to reload the page and capture a profile. Here’s what that looks like:
要查看页面加载期间发生的所有情况,我们将访问 Chrome DevTools 中的“性能”选项卡,然后单击“重新加载”按钮以重新加载页面并捕获配置文件。看起来是这样的:

Showing the first chunks of HTML streamed at the beginning of the timeline in DevTools.
Figure 10. (Large preview)
图 10.(大预览)

When we zoom in at the very beginning, we can see the first “Parse HTML” span. That’s the server streaming the first chunks of the document to the browser. The browser has just received the initial HTML, which contains the page shell and a few links to resources like fonts, CSS files, and JavaScript. The browser starts to invoke the scripts.
当我们从一开始放大时,我们可以看到第一个“Parse HTML”跨度。这是服务器将文档的第一部分流式传输到浏览器。浏览器刚刚收到初始 HTML,其中包含页面 shell 和一些指向字体、CSS 文件和 JavaScript 等资源的链接。浏览器开始调用脚本。

The first frames appear, and parts of the page are rendered
Figure 11. (Large preview)
图 11.(大预览)

After some time, we start to see the page’s first frames appear, along with the initial JavaScript scripts being loaded and hydration taking place. If you look at the frame closely, you’ll see that the whole page shell is rendered, and “loading” components are used in the place where there are suspended Server Components. You might notice that this takes place around 800ms, while the browser started to get the first HTML at 100ms. During those 700ms, the browser is continuously receiving chunks from the server.
一段时间后,我们开始看到页面的第一个框架出现,同时加载初始 JavaScript 脚本并发生水合作用。如果你仔细观察框架,你会发现整个页面外壳都被渲染出来,并且在暂停服务器组件的地方使用了“加载”组件。您可能会注意到,这发生在 800 毫秒左右,而浏览器在 100 毫秒时开始获取第一个 HTML。在这 700 毫秒内,浏览器不断从服务器接收数据块。

Bear in mind that this is a Next.js demo app running locally in development mode, so it’s going to be slower than when it’s running in production mode.
请记住,这是一个在开发模式下本地运行的 Next.js 演示应用程序,因此它会比在生产模式下运行时慢。

The Suspended Component #

Fast forward few seconds and we see another “Parse HTML” span in the page load timeline, but this one it indicates that a suspended Server Component finished loading and is being streamed to the browser.
快进几秒钟,我们在页面加载时间线中看到另一个“解析 HTML”范围,但这表明挂起的服务器组件已完成加载并正在流式传输到浏览器。

The suspended component’s HTML and RSC Payload are streamed to the browser, as shown in the developer tools Network tab.
Figure 12. (Large preview)
图 12.(大预览)

We can also see that a lazy-loaded Client Component is discovered at the same time, and it contains CSS and JavaScript files that need to be fetched. These files weren’t part of the initial bundle because the component isn’t needed until later on; the code is split into their own files.

This way of code-splitting certainly improves the performance of the initial page load. It also makes sure that the Client Component’s code is shipped only if it’s needed. If the Server Component (which acts as the Client Component’s parent component) throws an error, then the Client Component does not load. It doesn’t make sense to load all of its code before we know whether it will load or not.

Figure 12 shows the DOMContentLoaded event is reported at the end of the page load timeline. And, just before that, we can see that the localhost HTTP request comes to an end. That means the server has likely sent the last zero-sized chunk, indicating to the client that the data is fully transferred and that the streaming communication can be closed.
图 12 显示了在页面加载时间线末尾报告 DOMContentLoaded 事件。并且,就在这之前,我们可以看到 localhost HTTP 请求结束。这意味着服务器可能已经发送了最后一个零大小的块,向客户端表明数据已完全传输并且可以关闭流通信。

The End Result # 最终结果#

The main localhost HTTP request took around five seconds, but thanks to streaming, we began seeing page contents load much earlier than that. If this was a traditional SSR setup, we would likely be staring at a blank screen for those five seconds before anything arrives. On the other hand, if this was a traditional CSR setup, we would likely have shipped a lot more of JavaScript and put a heavy burden on both the browser and network.
主要的 localhost HTTP 请求花费了大约五秒的时间,但是由于流式传输,我们开始看到页面内容的加载时间比这要早得多。如果这是传统的 SSR 设置,我们可能会在任何内容到达之前盯着空白屏幕五秒钟。另一方面,如果这是传统的 CSR 设置,我们可能会发布更多的 JavaScript,并给浏览器和网络带来沉重的负担。

This way, however, the app was fully interactive in those five seconds. We were able to navigate between pages and interact with Client Components that have loaded as part of the initial main bundle. This is a pure win from a user experience standpoint.

Conclusion # 结论 #

RSCs mark a significant evolution in the React ecosystem. They leverage the strengths of server-side and client-side rendering while embracing HTML streaming to speed up content delivery. This approach not only addresses the SEO and loading time issues we experience with CSR but also improves SSR by reducing server load, thus enhancing performance.
RSC 标志着 React 生态系统的重大演变。他们利用服务器端和客户端渲染的优势,同时采用 HTML 流来加速内容交付。这种方法不仅解决了我们在 CSR 中遇到的 SEO 和加载时间问题,而且还通过减少服务器负载来提高 SSR,从而提高性能。

I’ve refactored the same RSC app I shared earlier so that it uses the Next.js Page router with SSR. The improvements in RSCs are significant:
我重构了之前分享的同一个 RSC 应用程序,以便它使用带有 SSR 的 Next.js 页面路由器。 RSC 的改进非常显着:

Comparing Next.js Page Router and App Router, side-by-side.
Figure 13. (Large preview)
图 13.(大预览)

Looking at these two reports I pulled from Sentry, we can see that streaming allows the page to start loading its resources before the actual request finishes. This significantly improves the Web Vitals metrics, which we see when comparing the two reports.
查看我从 Sentry 获取的这两个报告,我们可以看到流允许页面在实际请求完成之前开始加载其资源。这显着改善了 Web Vitals 指标,我们在比较两个报告时看到了这一点。

The conclusion: Users enjoy faster, more reactive interfaces with an architecture that relies on RSCs.
结论:用户可以通过依赖 RSC 的架构享受更快、更具反应性的界面。

The RSC architecture introduces two new component types: Server Components and Client Components. This division helps React and the frameworks that rely on it — like Next.js — streamline content delivery while maintaining interactivity.
RSC 架构引入了两种新的组件类型:服务器组件和客户端组件。该部门有助于 React 和依赖它的框架(例如 Next.js)简化内容交付,同时保持交互性。

However, this setup also introduces new challenges in areas like state management, authentication, and component architecture. Exploring those challenges is a great topic for another blog post!

Despite these challenges, the benefits of RSCs present a compelling case for their adoption. We definitely will see guides published on how to address RSC’s challenges as they mature, but, in my opinion, they already look like the future of rendering practices in modern web development.
尽管存在这些挑战,RSC 的优势仍为采用它们提供了令人信服的理由。随着 RSC 的成熟,我们肯定会看到有关如何应对 RSC 挑战的指南,但在我看来,它们已经成为现代 Web 开发中渲染实践的未来。

Smashing Editorial (gg, yk)  Smashing Editorial (gg,yk)

— Comments 3 — 评论3

Lai manh
Lai manh wrote #

Just want to say thank to you ^^

Lazar Nikolov
Lazar Nikolov wrote #

@Lai manh:Thank you, Lai!

Leave a comment 发表评论

Comments are moderated. They will be published only if they add to the discussion in a constructive way. If you disagree, please be polite. We all want to learn from each other here. We use GitHub Flavored Markdown for comments. Call out code within a sentence with single backticks (`command`). For a distinct block, use triple backticks (```code block```). Typo? Please let us know.
评论经过审核。只有当它们以建设性的方式加入讨论时才会被发布。如果您不同意,请保持礼貌。我们都想在这里互相学习。我们使用 GitHub Flavored Markdown 进行评论。用单反引号 ( `command` ) 调出句子中的代码。对于不同的块,请使用三个反引号 ( ```code block``` )。打字错误?请告诉我们。