我们趋行在人生这个亘古的旅途,在坎坷中奔跑,在挫折里涅槃,忧愁缠满全身,痛苦飘洒一地。 -- 《百年孤独》
大家好,我是柒八九。
前言
最近,无意中看到一篇关于React 18
的文章,翻看之后,发现很多东西都是React
官网没有细讲的,并且发现有些点也是在实际开发中可以实践和应用的.
同时,配合我们之前讲的关于浏览器性能优化
的文章,会对React
的应用有一个更深的了解.所以就有了这篇文章.
这里做一个内容披露
,因为下文中有很多关于React Server Components
的知识细节,我们只是会做简短的说明,后面我们会有一篇单独针对它的文章.(已经在快马加鞭的再写了,不信你看)
你能所学到的知识点
- 前置知识点
- 传统React渲染模式
- 过渡
- React Server Components
- Suspence
好了,天不早了,干点正事哇。
1. 前置知识点
前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
并发编程 VS 并行编程
我们在Rust并发中对这两个概念有过介绍,所以直接拿来主义了.
并发编程(Concurrent programming
)和并行编程(Parallel Programming
)都是指在计算机程序中同时执行多个任务或操作的编程方式,但它们在实现方式和目标上存在一些异同点。
并发编程
并发编程
指的是在一个程序中同时进行多个任务,这些任务可以是独立的,相互之间没有直接的依赖关系。
在并发编程中,这些任务通常是通过交替执行、时间片轮转或事件驱动的方式来实现并行执行的假象。
并发编程的目标是提高程序的效率、响应性和资源利用率。
并行编程
并行编程
是指在硬件级别上同时执行多个任务,利用计算机系统中的多个处理单元(例如多核处理器)或多台计算机来同时处理多个任务。
在并行编程中,任务之间可以有依赖关系,需要进行任务的分割和协调。
并行编程的目标是实现更高的计算性能和吞吐量。
并发编程
,代表程序的不同部分相互独立的执行,而并行编程
代表程序不同部分于同时执行。
主线程和长任务
当我们在浏览器中运行 JavaScript
时,JavaScript
引擎在一个单线程环境中执行代码,这通常被称为主线程。
主线程除了执行
JavaScript
代码外,还负责处理其他任务,包括处理用户交互(如点击和键入)、处理网络事件、定时器、更新动画以及管理浏览器的回流(reflow
)和重绘(repaint
)等。
当一个任务正在被处理时,所有其他任务必须等待。虽然浏览器可以顺利执行小型任务以提供流畅的用户体验,但长时间的任务可能会带来问题,因为它们会阻塞其他任务的处理。
任何执行时间超过 50 毫秒的任务被视为长任务。
这个 50 毫秒
的基准是基于设备必须每 16 毫秒
(60 帧每秒)创建一个新帧以保持流畅的视觉体验的事实。然而,设备还必须执行其他任务,比如响应用户输入和执行 JavaScript
。
这个 50 毫秒
的基准允许设备将资源分配给渲染帧和执行其他任务,并为设备提供额外的约 33.33 毫秒
的时间来执行其他任务,同时保持流畅的视觉体验。
关于为何以50 毫秒
为基准,我们在之前的浏览器之性能指标-TBT中介绍了 RAIL 性能模型
. 这里就不再过多介绍.
为了保持最佳性能,重要的是要尽量减少长任务的数量。为了衡量我们网站的性能,有两个指标可以衡量长任务对应用程序性能的影响:总阻塞时间(Total Blocking Time,简称 TBT)和与下一次绘制的交互(Interaction to Next Paint,简称 INP)。
TBT
是一个重要的指标,它衡量了从首次内容绘制(First Contentful Paint,简称 FCP)到可交互时间(Time to Interactive,简称 TTI)之间的时间。TBT
是长于 50 毫秒的任务执行所花费的时间之和。
如上图所示,TBT
是 45 毫秒
,因为在 TTI
之前有两个任务的执行时间超过了 50 毫秒的阈值,分别超出了 30 毫秒
和 15 毫秒
。
总阻塞时间是这些数值的累加:
30 毫
秒 +15 毫秒
=45 毫秒
INP
是一个新的Core Web Vitals
指标,它衡量了从用户首次
与页面进行交互(例如点击按钮)到该交互在屏幕上可见的时间;即下一次绘制的时间。这个指标对于包含许多用户交互的页面尤为重要。它通过累积用户在当前访问期间的所有 INP
测量值,并返回最差的分数来进行衡量。
INP
为 250 毫秒,因为这是最高的测量
可见延迟。
在之前的文章中,我们介绍了很多关于浏览器性能指标.如有兴趣,可以自行获取
React_Fiber
既然聊到了React 18
,那肯定绕不过去,React Fiber
的东西,而针对Fiber
的介绍,我们之前就有对应的文章.
在这两篇文章中,会出现React 元素(虚拟DOM)
/Fiber 节点
/副作用
/渲染算法
/调和机制
. 由于这些概念都很杂很多,如果想单独了解,请自行查找,这里不做解释了.
客户端渲染(CSR) 和服务端渲染(SSR)
CSR
页面托管服务器只需要对页面的访问请求响应一个如下的空页面
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <!-- metas --> <title></title> <link rel="shortcut icon" href="xxx.png" /> <link rel="stylesheet" href="xxx.css" /> </head> <body> <div id="root"><!-- page content --></div> <script src="xxx/filterXss.min.js"></script> <script src="xxx/x.chunk.js"></script> <script src="xxx/main.chunk.js"></script> </body> </html>
页面中留出一个用于填充渲染内容的视图节点 (div#root
),并插入指向项目编译压缩后的
JS Bundle
文件的script
节点- 指向
CSS
文件的link.stylesheet
节点等。
浏览器接收到这样的文档响应之后,会根据文档内的链接加载脚本与样式资源,并完成以下几方面主要工作:
- 执行脚本
- 进行网络访问以获取在线数据
- 使用 DOM API 更新页面结构
- 绑定交互事件
- 注入样式
其中,执行脚本就需要安装每个前端框架的内置方法,将JS代码生成对应的Virtual DOM
,然后在通过浏览器内置API将其转换为DOM
, 然后才会进行事件的绑定。
SSR
2. 传统React渲染模式
在
React
中,视觉更新分为两个阶段:渲染阶段和提交阶段。
渲染阶段是一个纯计算阶段,其中 React元素
与现有的 DOM
进行对比(也就是调和
)。这个阶段涉及创建一个新的 React 元素树,也称为虚拟 DOM
,它实质上是实际 DOM 的轻量级内存表示。
在渲染阶段,
React
计算当前 DOM
与新的 React 组件树
之间的差异,并准备必要的更新。
在渲染阶段之后是提交阶段。在这个阶段,React
将在渲染阶段计算得到的更新应用到实际 DOM 上。这涉及创建
、更新
和删除 DOM 节点
,以反映新的 React 组件树
。
在传统的同步渲染中,
React
对组件树中的所有元素赋予相同的优先级。
当组件树被渲染时,无论是在初始渲染
还是状态更新
时,React
会在一个不可中断的单一任务中渲染整个树,之后将其提交到 DOM
中,以在屏幕上更新组件的可视化效果。
同步渲染
是一种all-or-nothing
的操作,确保开始渲染的组件将始终完成渲染。根据组件的复杂性,渲染阶段可能需要一段时间才能完成。在此期间,主线程被阻塞,意味着用户在尝试与应用程序交互时会遇到无响应的用户界面,直到 React
完成渲染并将结果提交到 DOM
中。
假设存在这样的场景。有一个文本输入框
和一个包含大量城市的列表
,列表根据文本输入框当前的值进行过滤。在同步渲染中,React
会在每次键入时重新渲染 CitiesList
组件。这是一种非常昂贵的计算,因为列表包含成千上万个城市,因此在键入和在文本输入框中看到反映的过程中存在明显的视觉反馈延迟。
index.js
import { StrictMode } from "react"; import ReactDOM from "react-dom"; import App from "./App"; const rootElement = document.getElementById("root"); ReactDOM.render(<StrictMode><App /></StrictMode>, rootElement);
App.js
import React, { useState } from "react"; import CityList from "./CityList"; export default function SearchCities() { const [text, setText] = useState("太原"); return ( <main> <input type="text" onChange={(e) => setText(e.target.value) } /> <CityList searchQuery={text} /> </main> ); };
CityList.js
import cities from "cities-list"; import React, { useEffect, useState } from "react"; const citiesList = Object.keys(cities); const CityList = React.memo(({ searchQuery }) => { const [filteredCities, setCities] = useState([]); useEffect(() => { if (!searchQuery) return; setCities(() => citiesList.filter((x) => x.toLowerCase().startsWith(searchQuery.toLowerCase()) ) ); }, [searchQuery]); return ( <ul> {filteredCities.map((city) => ( <li key={city}> {city} </li> ))} </ul> ) }); export default CityList;
如果我们使用像
MacBook
这样的高端设备,我们可能希望将CPU
性能降低 4 倍,以模拟低端设备的情况。我们可以在开发者工具
中找到这个设置,路径是Devtools
>Performance
> ⚙️ >CPU
。
当我们查看性能选项卡时,可以看到每次输入都会发生长时间的任务,这是我们不能容忍的。
被标记为
红色角标
的任务被认为是长任务
。请注意总阻塞时间为4425.40毫秒
。
在这种情况下,React
开发者通常会使用像 debounce
这样的第三方库来延迟渲染
,但并没有内置的解决方案。
React 18
引入了一个新的并发渲染器,它在后台运行。这个渲染器为我们提供了一些方法来标记某些渲染为非紧急渲染。
当渲染低优先级组件(标记为
红色
)时,React
会让出主线程,以便检查是否有更重要的任务需要处理。
在这种情况下,React
将每隔 5 毫秒让出主线程,以查看是否有更重要的任务需要处理,比如用户输入,甚至是渲染其他 React
组件的状态更新,这些任务在当前时刻对用户体验更重要。通过持续地让出主线程,React 能够使这些渲染成为非阻塞的,并优先处理更重要的任务。这样可以改善用户体验并提高应用程序的性能。
与每次渲染一个单一的不可中断任务不同,新的并发渲染器在渲染低优先级组件时,在每个 5 毫秒的间隔内将控制权交还给主线程。
此外,并发渲染器能够在后台“并发”地渲染多个版本的组件树,而不立即提交结果。
与同步渲染是一种all-or-nothing
的计算方式不同, 并发渲染器允许 React
暂停和恢复渲染一个或多个组件树,以实现最优化的用户体验。这样的方式可以提高应用程序的性能,并确保用户界面的流畅和响应性。
当某个重要任务出现时,React
可以中断当前的渲染,转而处理该任务,然后在合适的时候继续渲染,避免了阻塞主线程和UI无响应的情况,从而提升了整体的渲染效率。
React
根据用户交互暂停当前的渲染,强制它优先渲染另一个更新。
借助并发特性,React
可以根据外部事件(如用户交互)暂停
和恢复
组件的渲染。当用户开始与 ComponentTwo
进行交互时,React
暂停当前的渲染,优先渲染ComponentTwo
,然后再继续渲染 ComponentOne
。