Remix v1 发布后,一个重要的问题不断出现
React 服务端组件怎么样?
好问题!像你们中的许多人一样,自从 2018 年首次宣布 React Suspense 以来,我们就一直在尝试它。事实上,Remix 的早期版本就使用了它。意识到它可能在我们准备好之前不会发布,我们将 Remix 的异步部分构建到了框架中,并且对结果非常满意。
现在,React 服务端组件(RSC)似乎越来越受到关注,我们再次进行了研究和测试,试图找出在 Remix 中利用它们的最佳方法。我们很清楚 RSC 仍处于实验阶段,因此我们不打算将此研究作为我们关于 RSC 和 Remix 的最终结论。我们只是认为,为了所有使用 Remix(和 React Router)并对使用 RSC 也感到好奇的人,分享我们的观点会很有用。
但首先,简单介绍一下背景:什么是 React 服务端组件?
在 Remix,我们绝对痴迷于用户体验(UX)。我们密切关注的一个主要方面是浏览器中的网络选项卡。如果网络选项卡一团糟,用户体验可能也会一团糟:弹跳的微调器、加载时间缓慢等等。如果网络选项卡干净,您的用户体验很可能快速且响应迅速。您的应用程序加载数据的方式会影响网络选项卡的形状。
在当今的 React 生态系统中,有三种方法可以将数据加载到您的应用程序中
渲染-获取瀑布(又名“边渲染边获取”):这指的是在组件内部,从浏览器中,在加载和渲染 JavaScript 包之后获取数据。我们称之为“瀑布”,因为在加载一个包、渲染并启动数据获取后,它会渲染执行相同操作的子组件。加载模块 → 渲染(微调器)→ 获取 → 渲染子组件(更多微调器)→ 在子组件中获取 → 等等。每次渲染和显示微调器都是瀑布中的另一步。
如果您还没有这样做,请向下滚动我们的主页,看看这种加载数据的方法如何影响 UI。它通过将这些资源与 UI 层次结构耦合来创建人为的数据和模块层次结构。在渲染之前,您不知道要获取什么,而且在获取父级数据之前,您无法渲染!这往往会在 UI 中产生“卡顿”,并导致 累积布局偏移 (CLS),因为子视图在父视图已经渲染后会弹出到页面中。
获取,然后渲染:在渲染页面之前,获取所有数据,然后一次渲染整个页面。这是 Remix 中的默认行为。这也是大多数网站几十年来的工作方式。由于嵌套路由,Remix 仅从 URL 就知道页面的所有依赖项(JS 模块、数据,甚至 CSS),因此它可以并行运行所有查询并加载资源。当您认为用户将要访问页面时,它甚至可以预取这些资源。您将在本文中看到,这对初始页面加载和后续导航有积极影响。
边获取边渲染:与获取,然后渲染类似,您可以并行启动所有加载,但您不会等待所有资源。相反,您会在准备好时渲染准备好的任何部分。除非您已经能够获取,然后渲染,否则这是不可能的。这是一个优化,可以尽快向用户提供有用的东西(而不是一个空的 div!)。
当今 React 生态系统中的几乎每个应用程序都使用渲染-获取瀑布。这是在 useEffect()
钩子内运行的任何数据获取的默认行为,包括 react-query
、useSWR
、Apollo Client 和许多其他库。
开箱即用,React 服务端组件也是一个渲染-获取瀑布。由于获取是在组件内部完成的,因此您的应用程序在组件渲染之前不知道要获取什么。
问题是,在这三者中,渲染-获取瀑布提供最差的用户体验。让我们运行一些测试,看看为什么。
我从 Facebook 的核心 React 团队中获取了 React 服务端组件演示,将代码重新排列到 Remix 的路由约定中,然后将两个版本都部署到澳大利亚的服务器,以便我们可以在美国真正感受到它(我现在非常想吃叻沙王。如果你知道,你就懂了)。
Remix 版本没有使用 React 服务端组件,它只是 React 17 上的普通 Remix。我的目标是看看我从每个版本中获得的开箱即用的性能,以及 Remix 从 RSC 中可以获益的地方。
虽然 RSC 仍然只是一个实验,这只是一个玩具应用程序,但我真的惊讶地发现Remix 在初始页面加载时速度是 RSC 的两倍多。(我上次在悉尼时应该吃两倍的叻沙王。)
如果您查看网络选项卡,您可以看到 Remix 如何并行加载资源,而 RSC 会导致级联的请求瀑布。获取代码,渲染,获取服务端组件,渲染。这个 UI 也没有任何嵌套,这就是为什么我如此惊讶地看到 Remix 的性能如此优于 RSC。加载嵌套的 UI 是 Remix 真正优于其他替代方案的地方。
但是,我也认识到 React 团队的演示并没有真正利用 React 服务端组件的杀手级功能,即在初始服务器渲染期间流式传输响应的能力。如果没有流式渲染,RSC 实际上只是另一种在组件内部获取数据的方式。
我决定针对也包含流式服务器渲染的 React 服务端组件演示进行衡量。我对这个功能(我想)已经兴奋了好几年了。我抓住了 Next.js Hacker News 克隆版,并将代码重新排列到 Remix 的数据加载约定中,以查看两者并排的感觉。然后我将两个应用程序都部署到 Vercel,以便它们在同一服务器上运行。
这次我完全预期 Remix 会输。
再次,即使没有 HTTP 缓存(真正的 HN 中包含用户数据,因此行不通),Remix 的速度也是 RSC 和 Next.js 的两倍多。此外,Remix 版本没有显示任何微调器,也没有任何内容布局偏移。
而且,这里也没有嵌套的 UI,但 Remix 的加载速度仍然比 Next.js + RSC + SSR 流式传输快 2 倍(使用陈旧时重新验证缓存时快 5 倍)。
RSC 的构建策略是边获取边渲染。RSC 本身不足以边获取边渲染。它需要一个位于其上方的框架才能在渲染之前启动并行加载资源。这两个演示都没有任何嵌套,这在嵌套中变得至关重要。
Facebook 有 Relay,一个奇特的编译器、后端基础设施和多个工程师团队,他们的收入远高于您和我,他们知道在渲染之前要获取什么。
但您有 Remix 🤗
在当今的环境下,Remix 拥有独特的优势,可以充分利用 Suspense、RSC 和 SSR 流式传输:它仅从 URL 就已经了解页面的所有信息,这正是 React 边获取边渲染所需要的。
此外,Remix 已经具有协同定位您的服务器和客户端代码的好处,包括 {name}.client.js
和 {name}.server.js
文件约定(通常不需要,但它会提示编译器哪些文件应该仅在一个地方运行)。
对于开发人员体验,Remix 路由模块已经是“服务端组件”。使用 RSC 只是 Remix 本身的一个实现细节。
当 RSC 准备好在 Remix 中采用时,迁移可能就像重命名您的路由文件一样简单
git mv routes/posts.tsx routes/posts.server.tsx
但是,我们将等待 RSC 稳定下来,并且没有我们在这里看到的性能和用户体验问题,然后再将其集成到 Remix 中。
真正的测试将是 Remix + RSC vs 仅 Remix。如果 Remix + RSC 提供更好的用户体验,我们将全力以赴。但是,当我们已经以 2 倍或更高的速度击败当前的演示时,很难证明投入这种努力是合理的。
再次,这些都是虚假应用程序的演示,因此我们不会太认真对待它们。但是,我们确实对 RSC 在网络选项卡上进行的权衡存在一个相当大的担忧。
似乎当今 Web 开发的时代精神是对初始页面加载和首字节时间(TTFB)的痴迷。但是,这里有一个同样重要的问题:在用户进入页面后会发生什么?
只痴迷于 TTFB 就像试图通过锻炼来增强体魄,但忽略了您的饮食(哎呀,这就是我做的!...但是,叻沙王听起来真不错)。
关上您的 M1X MacBook Pro,从学校拿起我孩子的 Chromebook,拔掉 CAT-6 电缆,然后跳上我岳父母的 WiFi。您会看到您的网站展现出完全不同的一面。
在低功耗设备上通过不稳定的网络花费一个小时阅读状态更新、更新记录、创建帖子和发送消息的用户体验,与 Remix 上任何网络上的任何设备上的初始页面加载一样重要。那么这与 RSC 有什么关系呢?
我“快速浏览”了 React 团队的演示,以查看应用程序显示每个页面需要多长时间。这是一个愚蠢的指标,但是当我使用一个缓慢的网站时,那种迟缓的感觉会随着时间的推移而加剧,并给我一种“这个网站不是那么好”的总体感觉。我认为这捕捉到了这种感觉。
正如您在此演示中看到的,RSC 版本通过网络加载的 JavaScript 比 Remix 版本多 34 倍(!)。
不,使用预先加载的浏览器缓存时,它是 16 倍
哦,对了。16 倍的 JavaScript?这到底是怎么回事?
除了流式渲染外,React 服务端组件的另一个重要功能是“零捆绑”。其想法是在初始页面加载时发送更少的数据以加快速度(我们之前看到在此演示中没有)。其想法是
<script>
的需要,该 <script>
中充满了已经在标记中重复的 JSON(在此站点上打开开发者工具,您会注意到此帖子在标记和底部的内联脚本标记中重复 ... 此外,所有这些错误都是 youtube 的,而不是我们的 😰)这表面上听起来很棒,但现在每次用户与网站互动时,模板都会在服务器组件的有效负载中重复。换句话说,每次您从服务器获取数据时,您都会获得完全渲染的标记,而不仅仅是数据。
在服务器组件演示中单击单个项目会导致为此(请注意:一个“服务器组件”)进行提取。
M1:{"id":22,"chunks":[2],"name":""}
M2:{"id":20,"chunks":[0],"name":""}
S3:"react.suspense"
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":"@4"}]}]]}],["$","section","3",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":"@5"}]}]]}]
M6:{"id":21,"chunks":[3],"name":""}
J4:["$","ul",null,{"className":"notes-list","children":[["$","li","1",{"children":["$","@6",null,{"id":1,"title":"Meeting Notes","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"This is an example note. It contains Markdown!"}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"Meeting Notes"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","2",{"children":["$","@6",null,{"id":2,"title":"A note with a very long title because sometimes you need more words","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"You can write all kinds of amazing notes in this app! These note live on the server in the notes..."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"A note with a very long title because sometimes you need more words"}],["$","small",null,{"children":"12/30/20"}]]}]}]}],["$","li","3",{"children":["$","@6",null,{"id":3,"title":"I wrote this note toda","expandedChildren":["$","p",null,{"className":"sidebar-note-excerpt","children":"It was an excellent note."}],"children":["$","header",null,{"className":"sidebar-note-header","children":[["$","strong",null,{"children":"I wrote this note toda"}],["$","small",null,{"children":"5:59 PM"}]]}]}]}]]}]
J5:["$","div",null,{"className":"note","children":[["$","div",null,{"className":"note-header","children":[["$","h1",null,{"className":"note-title","children":"I wrote this note toda"}],["$","div",null,{"className":"note-menu","role":"menubar","children":[["$","small",null,{"className":"note-updated-at","role":"status","children":["Last updated on ","3 Dec 2021 at 5:59 PM"]}],["$","@2",null,{"noteId":3,"children":"Edit"}]]}]]}],["$","div",null,{"className":"note-preview","children":["$","div",null,{"className":"text-with-markdown","dangerouslySetInnerHTML":{"__html":"<p>It was an excellent note.</p>\n"}}]}]]}]
那超过 4 kB。如果您单击所有三个项目,您会得到每个项目类似的响应。
让我们与 Remix 进行对比。第一次单击链接时,您必须下载项目视图的代码拆分 JavaScript 模板
import{a as i,c as a,d as p}from"/build/_shared/chunk-CDZR6LSD.js";import{a as m}from"/build/_shared/chunk-DQ7ZO7ZN.js";import"/build/_shared/chunk-XXRJHXMM.js";import{i as d}from"/build/_shared/chunk-2FSL4QX2.js";import{b as l,e as t,f as e}from"/build/_shared/chunk-AKSB5QXU.js";e();e();e();var f=l(p());function r(){let{id:n,title:s,body:u,updatedAt:o}=d();return o=new Date(o),t.createElement("div",{className:"note"},t.createElement("div",{className:"note-header"},t.createElement("h1",{className:"note-title"},s),t.createElement("div",{className:"note-menu",role:"menubar"},t.createElement("small",{className:"note-updated-at",role:"status"},"Last updated on ",i(o,"d MMM yyyy 'at' h:mm bb")),t.createElement(a,{noteId:n},"Edit"))),t.createElement(m,{body:u}))}export{r as default};
import{a as d}from"/build/_shared/chunk-XXRJHXMM.js";import{b as i,e as t,f as e}from"/build/_shared/chunk-AKSB5QXU.js";e();e();var n=i(d());function o({text:r}){return t.createElement("div",{className:"text-with-markdown",dangerouslySetInnerHTML:{__html:(0,n.default)(r)}})}function a({body:r}){return t.createElement("div",{className:"note-preview"},t.createElement(o,{text:r}))}export{a};
但从现在开始,每次点击项目只会传输这个小家伙
{"id": 1, "createdAt": "2020-12-30T10:13:29.023Z", "updatedAt":
"2020-12-30T10:13:29.023Z", "title": "Meeting Notes", "body": "This is an
example note. It contains **Markdown**!"}
因为服务器组件将您的数据与模板耦合在一起,您的用户每次与该组件相关的交互都必须下载模板。虽然对于您的 JavaScript 来说是“零捆绑”,但对于后续导航来说却是“无限捆绑” 😟。
当然,这是一个小玩具演示应用程序,在现实世界中情况总是会有所不同,但从逻辑上讲,这些模板总是比数据大。以我的经验,每一盎司数据都有一磅标记(天哪,我现在真想来一磅叻沙)。
我花了好几天摆弄这两个演示:在高速公路上用手机加载它们(当然不是在开车,但希望我能开车去叻沙王),在手机信号不好的小山上等着我的孩子放学,甚至在一个几乎没有手机信号的教堂里。
在所有情况下,无一例外,Remix 都比 React 服务器组件更快。而且快得多。
我不清楚 RSC 是为哪种网络、设备和服务器条件而构建的。从 Remix 发送整个文档总是比从 RSC 发送的第一个块快:无论网络速度快还是慢。
这并不是说 RSC “不好”。它仍然处于实验阶段!我只是说它们目前对 Remix 没有吸引力。当 RSC 稳定后,我们将应用我们在此处展示的相同严格测试,如果它们能提供更好的用户体验,我们将向 Remix 用户推荐它们。
我的直觉是,当用户的网络速度快但服务器的数据加载速度慢时,RSC 有机会提供更好的用户体验。这将是我的下一个研究方向(这些演示都具有快速的服务器数据加载速度)。我期望第一个块比您的慢速服务器通过 Remix 将完整文档发送给用户更快地对用户有用。
但如果问题出在您的慢速服务器上,您可以解决这个问题。您可以让您的服务器速度加快,但您无法对用户的网络做任何事情。而这正是 Remix 的优势,充分利用现代基础设施,减少网络传输的数据量。
后端基础设施正在变得非常好。Remix 可以在边缘(靠近您的用户)运行您的整个应用程序,例如在 Cloudflare Workers(请参阅我们的演示)和(即将推出)Deno Deploy 等平台上。您不仅可以在边缘运行您的应用程序服务器,还可以通过诸如 Fly.io Postgres 读取副本、Cloudflare KV 和 Durable Objects、FaunaDB 等技术将您的数据也推送到边缘。这些技术使您可以在短短几毫秒内渲染完整页面——即使包含用户数据!
如果您可以使用 Remix 在 500 毫秒甚至 50 毫秒内渲染包含用户数据的完整文档,您可能会问自己,为什么要使用四处弹跳的加载指示器来流式传输该文档(即使它今天快了两倍而不是慢了两倍)。
我们很想听听 React 团队本周在 React Conf 上关于 React 服务器组件和流式渲染的看法。目前,我们很高兴能够向核心团队提供我们的反馈,并希望我们的研究能够推动这项技术向前发展。请继续关注我们在这个领域的更多消息!
Hacker News 演示
Hacker News 源代码
Notes App 演示
Notes App 源代码