React 18 将包括对 React 服务器端渲染(SSR)性能的架构改进。这些改进是实质性的,并且是几年来工作的结晶。这些改进大多是在幕后进行的,但有一些选择性机制你需要注意,特别是如果你不使用框架的话。
主要的新 API 是 pipeToNodeWritable
,你可以在 Upgrading to React 18 on the Server 中了解到。我们计划在细节上做更多的实现,因为这不是最终版本,并且还有一些事情需要解决。
现有的主要的 API 是 <Suspense>
.
本文是对新的架构以及它的设计和所解决的问题的简单概述。
服务器端渲染(在这篇文章中缩写为 “SSR”)让你能在服务器上将 React 组件生成 HTML,并将该 HTML 发送给你的用户。SSR 能让你的用户在你的 JavaScript 包加载和运行之前看到页面的内容。
React 中的 SSR 总是分几个步骤进行:
关键在于,每一步都必须在下一步开始之前一次性完成整个应用程序的工作。如果你的应用程序的某些部分比其他部分慢,这样做的效率不高。这也是几乎所有具有一定规模的应用面临的问题。
React 18 让你使用 <Suspense>
来将你的应用程序分解成较小的独立单元。这些单元将独立完成这些步骤,并且不会阻碍应用程序的其他部分。因此,你的应用程序的用户将更快地看到内容,并能更快地开始与应用程序交互。你的应用程序中最慢的部分不会拖累那些较快的部分。这些优化是自动的。你不需要写任何特殊的代码来实现这个功能。
这也意味着 React.lazy
现在可以和 SSR 一起 “正常工作”。这里有一个 demo.
(如果你不使用框架,你将需要改变 HTML 生成的具体方式 wired up。)
当用户加载你的应用程序时,你希望尽快展示一个完全可交互的页面:
这幅插图用绿色来表达页面的可交互的部分。换句话说,它们所有的 JavaScript 事件处理程序都已经绑定好了,点击按钮可以更新状态等等。
然而,在页面的 JavaScript 代码完全加载之前,该页面是不能交互的。这包括 React 本身和你的应用程序代码。对于具有一定规模的应用程序,大部分的加载时间将用于下载你的应用程序代码。
如果你不使用 SSR,用户在 JavaScript 加载时看到的唯一东西就是一个空白的页面。
这不是很好,这就是为什么我们建议使用 SSR。SSR 让你在服务器上把你的 React 组件渲染成 HTML 并发送给用户。HTML 的交互性不强(除了简单的内置网络交互,如链接和表单输入)。但是,它能让用户在 JavaScript 仍在加载时看到一些东西。
这里,屏幕中灰色部分代表还没有完全可交互的部分。你的应用程序的 JavaScript 代码还没有加载完成,所以点击按钮是没有任何响应的。但特别是对于内容繁杂的网站,SSR 非常有用,因为它可以让网络连接较差的用户在 JavaScript 加载时开始阅读或查看内容。
当 React 和你的应用代码都在加载时,你要让这个 HTML 是可交互的。你告诉 React:“这是在服务器上生成这个 HTML 的 App
组件。将事件处理程序绑定到该 HTML 上!” React 会在内存中渲染你的组件树,但不是为其生成 DOM 节点,而是将所有逻辑绑定到现有的 HTML 上。
这个渲染组件和绑定事件处理程序的过程被称为 “hydration”。(这就像是用事件处理程序当作 “水” 来浇灌 “干燥” 的 HTML。至少,我是这样向自己解释这个术语的。)
hydration 之后,就是 “React 正常操作”:你的组件可以设置状态,响应点击等等:
你可以看到 SSR 有点像 “魔术”。它不能使你的应用程序更快地完全可交互。相反,它让你更快地展示你的应用程序的非交互式版本,以便用户在等待 JS 加载时可以查看静态内容。然而,这一招对于网络连接不畅的人来说有很大的不同,而且提高了整体的感知性能。它还有助于你的搜索引擎排名,既是因为有更容易的索引,也是因为有更快的响应速度。
注意:不要将 SSR 与服务器组件混淆。服务器组件是一个更具实验性的功能,目前仍在研究中,并且可能不会成为 React 18 最初版本的一部分。你从这里可以了解服务器组件。服务器组件是对 SSR 的补充,并将成为数据获取的推荐方式之一,但这篇文章并不介绍它们。
上述方法是可行的,但在许多方面,它并不是最佳的。
如今 SSR 的一个问题是,它不允许组件 “等待数据”。在目前的 API 中,当你渲染到 HTML 时,你必须已经在服务器上为你的组件准备好所有的数据。这意味着你必须在服务器上收集所有的数据,然后才能开始向客户端发送任何 HTML。这样是很低效的。
例如,假设你想渲染一个带有评论的帖子。尽早显示评论是很重要的,所以你要在服务器的 HTML 输出中包括它们。但你的数据库或 API 层很慢,这是你无法控制的。现在,你必须做出一些艰难的选择。如果你把它们从服务器输出中排除,在 JS 加载完毕之前,用户就不会看到它们。但如果你把它们包含在服务器输出中,你就必须推迟发送其余的 HTML(例如,导航栏、侧边栏,甚至是文章内容),直到评论加载完毕,你才能渲染完整的组件树。这样并不好。
顺便提一下,一些数据获取方案会反复尝试将树渲染成 HTML 并丢弃结果,直到数据被解决。因为 React 没有提供更符合人体工程学的选项。我们想提供一个不需要如此极端妥协的解决方案。
在你的 JavaScript 代码加载后,你会告诉 React 将 HTML “hydrate” 并使其具有交互性。React 在渲染你的组件时将 “走” 过服务器生成的 HTML,并将事件处理程序绑定到该 HTML 上。为了使其发挥作用,你的组件在浏览器中生成的树必须与服务器生成的树相匹配。否则 React 就不能 “匹配它们!” 这样做的一个非常不幸的后果是,你必须在客户端加载所有组件的 JavaScript,才能开始对任何组件进行 hydration
例如,假设评论小组件包含很多复杂的交互逻辑,并且需要花费一些时间为其加载 JavaScript。现在你不得不再次做出艰难的选择。把服务器上的评论渲染成 HTML,以便尽早显示给用户,这是一个好办法。但是,由于如今的 hydration 只能一次完成,所以在加载评论小组件的代码之前,你不能开始 hydrate 导航栏、侧边栏和文章内容。当然,你可以使用代码分割并单独加载,但你必须将注释从服务器 HTML 中排除。否则 React 将不知道如何处理这块 HTML(它的代码在哪里?),并在 hydration 过程中删除它。
hydration 本身也有一个类似的问题。如今,React 一次性完成树的 hydration。这意味着,一旦它开始 hydrate(本质上是调用你的组件函数),React 就不会停止 hydration 的过程,直到它为整个树完成 hydration。因此,你必须等待所有的组件被 hydrated,才能与任何组件进行交互。
例如,我们说评论小组件有昂贵的渲染逻辑。它在你的电脑上可能运行得很快,但在低端设备上运行这些逻辑的成本并不低,甚至可能使得屏幕被锁定好几秒钟。当然,在理想情况下,我们在客户端不会这样的逻辑(这是服务器组件可以帮助解决的问题)。但对于某些逻辑来说,这是不可避免的。这是因为它决定了所附的事件处理程序应该做什么,而且对于交互性是至关重要的。因此,一旦开始 hydration,用户就不能与导航栏、侧边栏或文章内容互动,直到整棵树完成 hydration。对于导航来说,这是特别不幸的,因为用户可能想完全离开这个页面,但由于我们正忙于 hydration,我们把他们留在他们不再关心的当前页面上。
这些问题之间有一个共同点。它们迫使你在早做一些事情(但因为它阻碍了所有其他工作,导致用户体验被损害),或晚做一些事情(但因为你浪费时间,导致用户体验被损害)之间做出选择。
这是因为有一个 “瀑布”(流程):获取数据(服务器)→ 渲染成 HTML(服务器)→ 加载代码(客户端)→ hydration(客户端)。任何一个阶段都不能在前一个阶段结束之前开始。这就是为什么它的效率很低。我们的解决方案是将工作分开,这样我们就可以为屏幕的一部分而不是整个应用程序做这些阶段的工作。
这并不是一个新奇的想法:比如说:Marko 是实现该模式的一个 JavaScript 网络框架。将这样的模式适应于 React 编程模型具有一定的挑战性。我们也因此花了一段时间来解决这个难题。我们在 2018 年为此目的引入了 <Suspense>
组件。当我们引入它时,我们只支持它在客户端进行惰性加载代码。但我们的目标是将它与服务器渲染结合起来,解决这些问题。
让我们看看如何在 React 18 中使用 <Suspense>
来解决这些问题。
在 React 18 中,有两个主要的 SSR 功能是由 Suspense 解锁的。
renderToString
切换到新的 pipeToNodeWritable
方法,如此处描述。createRoot
,然后开始用 <Suspense>
包装你的应用程序的一部分。为了了解这些功能的作用以及它们如何解决上述问题,让我们回到我们的例子。
如今的 SSR 中,渲染 HTML 和 hydration 是 “全有或全无” 的。首先,你要渲染所有的 HTML:
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section>
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</section>
</main>
客户端最终会收到它:
然后你加载所有的代码,并对整个应用程序进行 hydration:
但是 React 18 给了你一个新的可能性。你可以用 <Suspense>
来包装页面的一部分。
例如,让我们包裹评论块并告诉 React,在它准备好之前,React 应该显示 <Spinner />
组件。
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
通过将 <Comments>
包装成 <Suspense>
,我们告诉 React,它不需要等待评论就可以开始为页面的其他部分传输 HTML。相反,React 将发送占位符(一个旋转器)而不是评论:
现在在最初的 HTML 中找不到评论了:
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width=400 src="spinner.gif" alt="Loading..." />
</section>
</main>
事情到这里还没有结束。当服务器上的评论数据准备好后,React 会将额外的 HTML 发送到同一个流中,以及一个最小的内联 <script>
标签,将 HTML 放在 “正确的地方”。
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// This implementation is slightly simplified
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script>
因此,甚至在 React 本身加载到客户端之前,迟来的评论的 HTML 就会 “弹出”。
这就解决了我们的第一个问题。现在你不必在显示任何东西之前获取所有的数据了。如果屏幕的某些部分延迟了最初的 HTML,你就不必在延迟所有的 HTML 或将其排除在 HTML 之外之间做出选择。你可以只允许那部分内容在 HTML 流中稍后 “涌入”。
不同于传统的流式 HTML,它不一定要按照自上而下的顺序发生。例如,如果侧边栏需要一些数据,你可以用 Suspense 包装它,React 将会发出一个占位符,然后继续渲染帖子。然后,当侧边栏的 HTML 准备好了,React 会把它和 <script>
标签一起流出来,把它插入到正确的位置 ——— 尽管帖子的 HTML(在树中更远的地方)已经被发送出去了!没有要求数据以任何特定的顺序加载。你指定旋转器应该出现在哪里,剩下的就由 React 来解决。
注意事项:为了使其发挥作用,你的数据获取解决方案需要与 Suspense 集成。服务器组件将与 Suspense 开箱即用,但我们也将为独立的 React 数据获取库提供一种方法来与之集成。
我们可以提前发送最初的 HTML,但我们仍然有一个问题。在加载评论小组件的 JavaScript 代码之前,我们不能在客户端开始对我们的应用程序进行 hydration。如果代码的大小很大,这可能需要一段时间。
为了避免大型包,你通常会使用 “代码拆分”:你可以指定一段代码不需要同步加载,你的打包工具将把它分割成一个单独的 <script>
标签。
你可以使用 React.lazy
进行代码分割,将评论代码从主包中分割出来。
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
以前,这与服务器渲染中是不奏效的。(据我们所知,即使是流行的变通方法也迫使你在选择不使用代码拆分组件的 SSR 或在所有代码加载后对其进行 hydration 之间做出选择,这在某种程度上违背了代码拆分的目的)。
但在 React 18 中,<Suspense>
可以让你在评论小组件加载之前就 hydrate 应用程序。
从用户的角度来看,最初他们看到的是以 HTML 形式流进来的非交互式内容。
然后你告诉 React 进行 hydration。虽然评论的代码还没有出现,但也没关系:
这是一个选择性 hydration 的例子。通过将 Comments
包裹在 <Suspense>
中,你告诉 React,他们不应该阻止页面的其他部分进行流式传输 ——— 而且,事实证明,也不应该阻止 hydration。这意味着第二个问题已经解决了:你不再需要等待所有的代码加载完成,才能开始 hydration。React 可以在加载部分时同时进行 hydration。
React 会在评论部分的代码加载完毕后开始对其部分进行 hydration:
得益于选择性 hydration,一块沉重的 JS 并不妨碍页面的其他部分具有交互性。
React 会自动处理这一切,所以你不需要担心事情会以意外的顺序发生。例如,也许 HTML 需要一段时间来加载,即使它正在被流化:
如果 JavaScript 代码的加载时间早于所有的 HTML,React 就没有理由等待了!它将为页面的其他部分进行 hydration:
当评论的 HTML 加载时,因为 JS 还没有出现,所以它将显示为非交互式:
最后,当评论小组件的 JavaScript 代码加载时,页面将变得完全可交互:
当我们将评论包裹在 <Suspense>
中时,还有一项改进发生在幕后。现在它们的 hydration 不再阻碍浏览器做其他工作。
例如,假设用户在评论正在 hydration 时点击了侧边栏:
在 React 18 中,浏览器可以在给 Suspense 里的内容进行 hydration 的过程中出现的微小空隙中进行事件处理。得益于此,点击被立即处理,在低端设备上长时间的 hydration 过程中,浏览器不会出现卡顿。例如,这可以让用户从他们不再感兴趣的页面上导航离开。
在我们的例子中,只有评论被包裹在 Suspense 中,所以对页面的其他部分进行 hydration 是一次性的。然而,我们可以通过在更多的地方使用 Suspense 来解决这个问题。例如,让我们把侧边栏也包起来。
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
现在两者都可以在包含导航条和帖子的初始 HTML 之后从服务器上流传。但这也会对 hydration 产生影响。比方说,它们的 HTML 已经加载,但它们的代码还没有加载:
然后,包含侧边栏和评论代码的包被加载。React 将尝试对它们进行 hydration,从它在树中较早发现的 Suspense 边界开始(在这个例子中,它是侧边栏):
但是,假设用户开始与评论小组件进行互动,其代码也被加载:
React 会记录点击,并优先给评论进行 hydration,因为它更紧急:
在评论被 hydrated 后,React “重放” 记录的点击事件(通过再次派发),并让你的组件对互动做出反应。然后,现在 React 没有什么紧急的事情要做,因此 React 会给侧边栏进行 hydration:
这解决了我们的第三个问题。得益于选择性 hydration,我们不必 “为了与任何东西互动而对所有东西进行 hydration”。React 尽早开始给所有东西进行 hydration。它根据用户的互动情况,优先考虑屏幕上最紧急的部分。如果你考虑到在整个应用程序中采用 Suspense,边界将变得更加细化,那么选择性 hydration 的好处就更加明显:
在这个例子中,用户点击第一条评论时,正好是 hydration 的开始。React 会优先给所有父级 Suspense 边界的内容进行 hydration,但会跳过任何不相关的兄弟节点。因为交互路径上的组件优先被 hydrated,这创造了 hydration 是即时的的错觉。React 会在之后对应用程序的其他部分进行 hydration。
在实践中,你可能会在你的应用程序的根部附近添加 Suspense:
<Layout>
<NavBar />
<Suspense fallback={<BigSpinner />}>
<Suspense fallback={<SidebarGlimmer />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<CommentsGlimmer />}>
<Comments />
</Suspense>
</RightPane>
</Suspense>
</Layout>
在这个例子中,最初的 HTML 可以包括 <NavBar>
的内容,但其余的内容会在相关代码加载后立即流入,并分部分进行 hydration,优先考虑用户互动过的部分。
注意:你可能想知道你的应用程序如何能在这种不完全 hydrated 的状态下运作。设计中有一些微妙的细节,使其发挥作用。例如,不是对每个单独的组件分别进行 hydration,而是对整个
<Suspense>
边界进行 hydration。因为<Suspense>
已经被用于不会立即出现的内容,所以你的代码对它的孩子不能立即出现的情况有自适应性。React 总是以父级优先的顺序进行 hydration,所以组件总是有它们的 props 组合。React 在事件发生地的整个父树 hydration 之前,暂不分派事件。最后,如果父类的更新方式导致尚未 hydrated 的 HTML 变得陈旧,React 将隐藏它,并用你指定的fallback
来代替它,直到代码加载完毕。这确保了树在用户面前显得一致。你不需要考虑这个,但这就是该功能发挥作用的原因。
我们准备了一个 你可以尝试的演示,看看新的 Suspense SSR 架构如何运作。它被人为地放慢了速度,所以你可以在 server/delays.js
中调整延时。
API_DELAY
让你使评论在服务器上需要更长的时间来获取,展示 HTML 的其他部分如何提前发送。JS_BUNDLE_DELAY
让你延迟 <script>
标签的加载,展示评论小组件的 HTML 如何在 React 和你的应用程序包下载之前 “弹出”。ABORT_DELAY
让你看到服务器 “放弃”,并在服务器上获取时间过长时将渲染工作移交给客户端。React 18 为 SSR 提供了两个主要功能:
<script>
标签一起放在正确的地方。这些功能解决了 React 中 SSR 的三个长期存在的问题:
Suspense
组件作为所有这些功能的选择。这些改进本身是在 React 内部自动进行的,我们期望它们能与大多数现有的 React 代码一起使用。这展示了声明性地表达加载状态的力量。从 if (isLoading)
到 <Suspense>
可能看不出很大的变化,但它却是解锁这些改进的关键。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/XY0ko6MFy0ag3e_QGF6JDg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。