在过去的几年中,React 的流行度一直在增加,而且还在加速。React 每周的 npm 下载量超过 1400 万次 ,React Devtools Chrome 扩展有超过 300 万 的周活跃用户。
然而,在 React 18 之前,React 中的渲染模式几乎是相同的。在本文中,我们将研究 React 当前的渲染模式、它们存在的问题,以及 React 18 引入的新模式如何是解决这些问题的。
在深入研究渲染模式之前,让我们来看一下将在这篇文章中使用的一些重要术语:
目前,我们在 React 中使用的最常见的模式就是客户端渲染和服务端渲染,以及由 Next.js等框架提供的一些高级形式的服务端渲染,例如静态站点生成(SSG)、增量静态生成(ISR)。我们将研究其中这其中的每一个,并深入研究 React 18 引入的新模式。
在 Next.js 和 Remix 等元框架出现之前,客户端渲染(主要使用 create-react-app 或其他类似的脚手架)是构建 React 应用程序的默认方式。
使用客户端渲染,服务端只需要为包含必要<script>
和<link>
标签的页面提供基本 HTML。一旦相关的 JavaScript 下载到浏览器。 React 渲染树并生成所有 DOM 节点。 路由和数据获取的所有逻辑也由客户端 JavaScript 处理。
为了看看 CSR 是如何工作的,我们来渲染以下应用:
<Layout>
<Navbar />
<Sidebar />
<RightPane>
<Post />
<Comments />
</RightPane>
</Layout>
渲染周期如下:
CSR渲染周期.gif
这是客户端渲染的 Network 图:
因此,CSR 应用接收到应答数据第一个字节很快(TTFB),因为它们主要依赖于静态资源。 但是,在下载相关 JavaScript 之前,用户必须盯着空白屏幕。 在那之后,由于大多数应用都需要从 API 获取数据并向用户显示相关数据,这导致加载页面主要内容所需的时间(LCP)很长。
目前,在 React 中服务端渲染的工作方式如下:
renderToString
获取相关数据并在服务端为页面运行客户端 JavaScript,这为我们提供了显示页面所需的所有 HTML。为了更好地理解它是如何工作的,让我们来看一下上面例子中使用 SSR 时的生命周期:
<Layout>
<Navbar />
<Sidebar />
<RightPane>
<Post />
<Comments />
</RightPane>
</Layout>
渲染周期如下:
SSR渲染周期.gif
这是服务端渲染的 Network 图:
因此,使用 SSR,我们可以获得良好的 FCP 和 LCP,但 TTFB 会受到影响,因为我们必须在服务端获取数据,然后将其转换为 HTML 字符串。
现在,你可能会问这和 Next.js 的 SSG/ISR 有啥区别呢?它们也必须经历上面的过程。 唯一的区别是,它们不会受到 TTFB 时间较长的影响。因为 HTML 要么是在构建时生成的,要么是在请求传入时以增量方式生成和缓存的。
但是,SSG/ISR 更适合公共页面。对于根据用户登录状态或浏览器上存储的其他 cookie 更改的页面,必须使用 SSR。
上面,我们介绍了 React 中当前的渲染模式是什么以及它们存在什么问题。 总结一下:
React 团队正在研究一些旨在解决这些问题的新模式。
浏览器可以通过 HTTP 流接收 HTML。流式传输允许 Web 服务端通过单个 HTTP 连接将数据发送到客户端,该连接可以无限期保持打开状态。因此,我们可以通过网络以多个块的形式在浏览器上加载数据,这些数据在渲染时按顺序加载。
流式渲染并不是 React 18 中全新的东西。事实上,它从 React 16 开始就存在了。React 16 有一个名为 renderToNodeStream
的方法,与 renderToString
不同,它将前端渲染为浏览器的 HTTP 流。
这允许在渲染它的同时以块的形式发送 HTML,从而为用户提供更快的 TTFB 和 LCP,因为初始 HTML 更快地到达浏览器。
React 18 弃用了 renderToNodeStream
API,取而代之的是一个名为 renderToPipeableStream
的新 API,它通过 Suspense 解锁了一些新功能,允许将应用分解为更小的独立单元,这些单元可以独立完成我们在 SSR 中看到的步骤。这是因为 Suspense 添加了两个主要功能:
① 服务端流式渲染
如上所述,React 18 之前的 SSR 是一种全有或全无的方法。 首先,需要获取页面所需的数据,并生成 HTML,然后将其发送到客户端。 由于 HTTP 流,情况不再如此。
在 React 18 中想要使用这种方式,可以包装可能需要较长时间才能加载且在 Suspense 中不需要立即显示在屏幕上的组件。
为了了解它的工作原理,假设 Comments API 很慢,所以我们将 Comments 组件包装在Suspense 中:
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
这样,初始 HTML 中就不存在 Comments,返回的只有占位的 Spinner:
<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>
最后,当数据准备好用于服务端的 Comments 时,React 将发送最少的 HTML 到带有内联<script>
标签的同一流中,以将 HTML 放在正确的位置:
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// 简化了实现
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script>
因此,这解决了第一个问题,因为现在不需要等待服务端获取所有数据,浏览器可以开始渲染应用的其余部分,即使某些部分尚未准备好。
② 客户端选择性注水
即使 HTML 被流式传输,页面也不会可交互的,除非页面的整个 JavaScript 被下载完。这就是选择性注水的用武之地。
在客户端渲染期间避免页面上出现大型包的一种方法就是通过 React.lazy 进行代码拆分。 它指定了应用的某个特定部分不需要同步加载,并且打包工具会将其拆分为单独的<script>
标签。
React.lazy 的限制是它不适用于服务端渲染。但在 React 18 中,<Suspense>
除了允许流式传输 HTML 之外,它还可以为应用的其余部分注水。
所以,现在 React.lazy 在服务端开箱即用。 当你将 lazy 组件包裹在 <Suspense>
中时,不仅告诉 React 你希望它被流式传输,而且即使包裹在 <Suspense>
中的组件仍在被流式传输,也允许其余部分注水。这也解决了我们在传统服务端渲染中看到的第二个问题。在开始注水之前,不再需要等待所有 JavaScript 下载完毕。
下面,我们把 Comments 包含在 Suspense 中,并使用新的 Suspense 架构,来看看应用的生命周期:
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
渲染周期如下:
流式 SSR 渲染周期.gif
这就产生了像下面这样的 Network 图:
这个例子想说明的是,对于 Suspense,很多连续发生的事情现在可以并行发生。
这不仅有助于我们在 HTML 被流式传输后更快地 TTFB,而且用户不必等待所有 JavaScript 被下载才能开始与应用交互。 除此之外,它还有助于在页面开始流式传输时立即加载其他资源(CSS、JavaScript、字体等),有助于并行更多请求。
另外,如果有多个组件包裹在 Suspense 中并且还没有在客户端上注水,但是用户开始与其中一个交互,React 将优先考虑给该组件注水。
上面,我们介绍了如何通过将应用分解为更小的单元并分别对它们进行流式处理和选择性注水来提高服务端渲染性能。 但是,如果有一种方法可以完全不需要对应用的某些部分进行注水呢?
这就是全新的 Server Components RFC 的用武之地。它旨在补充服务端渲染,允许拥有仅在服务端渲染且没有交互性的组件。
它们的工作方式就是可以使用 .server.js/jsx/ts/tsx
扩展创建非交互式服务端组件,然后它们可以无缝集成并将 props 传递给客户端组件(使用 .client.js/jsx/ts/tsx
扩展),它可以处理页面的交互部分。以下是它提供的功能的:
(1)不影响客户端包
服务端组件仅在服务端渲染,不需要注水。它允许我们在服务端渲染静态内容,同时对客户端包大小没有影响。 如果使用的是繁重的库并且没有交互性,这可能特别有用,并且它可以完全渲染在服务端,而不会影响客户端包。 RFC 中的 Notes 预览就是一个很好的例子:
// NoteWithMarkdown.js
// 在 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 */);
}
// NoteWithMarkdown.server.js - Server Component === 包大小为0
import marked from 'marked'; // 包大小为0
import sanitizeHtml from 'sanitize-html'; // 包大小为0
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* render */);
}
(2)服务端组件不具有交互性,但可以与客户端组件组合
由于它们只在服务端渲染,它们只是接收 props 并渲染视图的 React 组件。 因此,它们不能像常规客户端组件中那样拥有状态、effects 和事件处理程序之类的东西。
尽管它们可以导入具有交互性的客户端组件,并且在客户端上渲染时注水,正如我们在普通 SSR 中看到的那样。 客户端组件与服务端组件类似,使用 .client.jsx
或 .client.tsx
后缀定义。
这种可组合性使开发人员在页面上节省大量的包大小,例如具有大部分静态内容和很少交互元素的详情页。 例如:
// Post.server.js
import { parseISO, format } from 'date-fns';
import marked from 'marked';
import sanitizeHtml from 'sanitize-html';
import Comments from '../Comments.server.jsx'
// 导入客户端组件
import AddComment from '../AddComment.client.jsx';
function Post({ content, created_at, title, slug }) {
const html = sanitizeHtml(marked(content));
const formattedDate = format(parseISO(created_at), 'dd/MM/yyyy')
return (
<main>
<h1>{title}</h1>
<span>Posted on {formattedDate}</span>
{content}
<AddComment slug={slug} />
<Comments slug={slug} />
</main>
)
}
// AddComment.client.js
function AddComment({ hasUpvoted, postSlug }) {
const [comment, setComment] = useState('');
function handleCommentChange(event) {
setComment(event.target.value);
}
function handleSubmit() {
// ...
}
return (
<form onSubmit={handleSubmit}>
<textarea name="comment" onChange={handleCommentChange} value={comment}/>
<button type="submit">
Comment
</button>
</form>
)
}
上面的代码是服务端组件如何与客户端组件组合的示例。 让我们来分解一下:
这里,我们在服务端组件中导入的所有日期和 markdown 解析库都不会在客户端下载。我们在客户端下载的唯一 JavaScript 就是 AddComment 组件。
(3)服务端组件可以直接访问后端
由于它们仅在服务端渲染,因此可以使用它们直接从组件访问数据库和其他仅限后端的数据源,如下所示:
// Post.server.js
import { parseISO, format } from 'date-fns';
import marked from 'marked';
import sanitizeHtml from 'sanitize-html';
import db from 'db.server';
// 导入客户端组件
import Upvote from '../Upvote.client.js';
function Post({ slug }) {
// 直接从数据库中读取数据
const { content, created_at, title } = db.posts.get(slug);
const html = sanitizeHtml(marked(content));
const formattedDate = format(parseISO(created_at), 'dd/MM/yyyy');
return (
<main>
<h1>{title}</h1>
<span>Posted on {formattedDate}</span>
{content}
<AddComment slug={slug} />
<Comments slug={slug} />
</main>
);
}
现在你可能会说,在传统的服务端渲染中也可以实现这一点。 例如,Next.js 可以直接在 getServerSideProps
和 getStaticProps
中访问服务端数据。 没错,但区别在于,传统的 SSR 是一种全有或全无的方法,只能在顶级页面上完成,但服务端组件可以在每个组件的基础上执行此操作。
(4)自动代码拆分
代码拆分是一个概念,它允许将应用分成更小的块,向客户端发送更少的代码。对应用进行代码拆分的最常见方式就是按路由进行拆分。这也是 Next.js 等框架默认拆分包的方式。
除了自动代码拆分之外,React 还允许使用 React.lazy API 在运行时延迟加载不同的模块。 这又是一个来自 RFC 的很好的例子,说明这可能特别有用:
// PhotoRenderer.js
// 在 Server Components 之前
import React from 'react';
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));
function Photo(props) {
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <OldPhotoRenderer {...props} />;
}
}
这种技术通过在运行时只动态导入需要的组件来提高性能,但它确实有一些问题。 例如,这种方法会延迟应用开始加载代码的时间,从而抵消了加载更少代码的好处。
正如我们之前在客户端组件如何与服务器组件组合中看到的那样,它们通过将所有客户端组件导入视为潜在的代码拆分点,并允许开发人员选择要在服务端更早渲染的内容,从而使客户端能够更早下载。 下面是 RFC 中使用服务端组件的相同 PhotoRenderer 示例:
// PhotoRenderer.server.js - Server Component
import React from 'react';
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';
function Photo(props) {
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <OldPhotoRenderer {...props} />;
}
}
服务端组件可以在保留客户端状态的同时重新加载:我们可以随时从客户端重新获取服务端树,以从服务端获取更新的状态,而不会破坏本地客户端状态、焦点甚至正在进行的动画。
这是可能的,因为接收到的 UI 描述是数据而不是纯 HTML,这允许 React 将数据合并到现有组件中,从而使客户端状态不会被破坏。
(5)服务端组件与 Suspense 集成
服务器组件可以通过 <Suspense
> 逐步流式传输,正如在上面中看到的那样,这允许我们在等待页面剩余部分加载时创建加载状态并快速显示重要内容。
接下来看看上面的例子在使用 React 服务端组件时是什么样的。 这次 Sidebar 和 Post 是服务端组件,而 Navbar 和 Comments 是客户端组件。 我们也将 Post 包裹在 Suspense 中。
<Layout>
<NavBar />
<SidebarServerComponent />
<RightPane>
<Suspense fallback={<Spinner />}>
<PostServerComponent />
</Suspense>
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
渲染周期如下:
Server components 渲染周期.gif
它的 Network 图与使用 Suspense 的流式渲染非常相似,但 JavaScript 更少。因此,服务端组件甚至是解决我们一开始的问题的进一步措施,它不仅可以下载更少的 JavaScript,而且还显着改善了开发者体验。
React 团队还在 RFC 常见问题解答中提到,他们在 Facebook 的单个页面上对少数用户进行了实验,产品代码大小减少了约 30%。
目前,服务端组件仍处于 alpha 阶段,而具有新 Suspense 架构的流式 SSR 所需的用于数据获取的 Suspense 还没有正式发布,将在 React 18 的小更新中发布。
在这里查看 React 团队和 Next.js 团队的演示:
参考文章:https://prateeksurana.me/blog/future-of-rendering-in-react/
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ilgMM-7I8VEj3jrXMjjvIg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为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 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。