React 18 如何提升应用性能(一)

简介: React 18 如何提升应用性能(一)

我们趋行在人生这个亘古的旅途,在坎坷中奔跑,在挫折里涅槃,忧愁缠满全身,痛苦飘洒一地。 -- 《百年孤独》

大家好,我是柒八九

前言

最近,无意中看到一篇关于React 18的文章,翻看之后,发现很多东西都是React官网没有细讲的,并且发现有些点也是在实际开发中可以实践和应用的.

同时,配合我们之前讲的关于浏览器性能优化的文章,会对React的应用有一个更深的了解.所以就有了这篇文章.

这里做一个内容披露,因为下文中有很多关于React Server Components的知识细节,我们只是会做简短的说明,后面我们会有一篇单独针对它的文章.(已经在快马加鞭的再写了,不信你看)

你能所学到的知识点

  1. 前置知识点
  2. 传统React渲染模式
  3. 过渡
  4. React Server Components
  5. Suspence

好了,天不早了,干点正事哇。



1. 前置知识点

前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

并发编程 VS 并行编程

我们在Rust并发中对这两个概念有过介绍,所以直接拿来主义了.

并发编程(Concurrent programming)和并行编程(Parallel Programming)都是指在计算机程序中同时执行多个任务或操作的编程方式,但它们在实现方式和目标上存在一些异同点。

并发编程

并发编程指的是在一个程序中同时进行多个任务,这些任务可以是独立的,相互之间没有直接的依赖关系

在并发编程中,这些任务通常是通过交替执行、时间片轮转或事件驱动的方式来实现并行执行的假象。

并发编程的目标是提高程序的效率、响应性和资源利用率

并行编程

并行编程是指在硬件级别上同时执行多个任务,利用计算机系统中的多个处理单元(例如多核处理器)或多台计算机来同时处理多个任务

在并行编程中,任务之间可以有依赖关系,需要进行任务的分割和协调

并行编程的目标是实现更高的计算性能和吞吐量


并发编程,代表程序的不同部分相互独立的执行,而 并行编程代表程序不同部分于同时执行


主线程和长任务

当我们在浏览器中运行 JavaScript 时,JavaScript 引擎在一个单线程环境中执行代码,这通常被称为主线程

主线程除了执行 JavaScript 代码外,还负责处理其他任务,包括处理用户交互(如点击和键入)、处理网络事件、定时器、更新动画以及管理浏览器的回流(reflow)和重绘(repaint)等。

image.png

当一个任务正在被处理时,所有其他任务必须等待。虽然浏览器可以顺利执行小型任务以提供流畅的用户体验,但长时间的任务可能会带来问题,因为它们会阻塞其他任务的处理。


任何执行时间超过 50 毫秒的任务被视为长任务

image.png

这个 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 毫秒的任务执行所花费的时间之和

image.png

如上图所示,TBT45 毫秒,因为在 TTI 之前有两个任务的执行时间超过了 50 毫秒的阈值,分别超出了 30 毫秒15 毫秒

总阻塞时间是这些数值的累加:30 毫秒 + 15 毫秒 = 45 毫秒

INP是一个新的Core Web Vitals 指标,它衡量了从用户首次与页面进行交互(例如点击按钮)到该交互在屏幕上可见的时间;即下一次绘制的时间。这个指标对于包含许多用户交互的页面尤为重要。它通过累积用户在当前访问期间的所有 INP 测量值,并返回最差的分数来进行衡量。

image.png

INP为 250 毫秒,因为这是最高的测量可见延迟。

在之前的文章中,我们介绍了很多关于浏览器性能指标.如有兴趣,可以自行获取

  1. 浏览器之性能指标_FCP
  2. 浏览器之性能指标-LCP
  3. 浏览器之性能指标-CLS
  4. 浏览器之性能指标-FID
  5. 浏览器之性能指标-TTI
  6. 浏览器之性能指标-TBT
  7. 浏览器之性能指标-INP

React_Fiber

既然聊到了React 18,那肯定绕不过去,React Fiber的东西,而针对Fiber的介绍,我们之前就有对应的文章.

  1. React_Fiber(上)
  2. React_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 节点等。

浏览器接收到这样的文档响应之后,会根据文档内的链接加载脚本与样式资源,并完成以下几方面主要工作:

  1. 执行脚本
  2. 进行网络访问以获取在线数据
  3. 使用 DOM API 更新页面结构
  4. 绑定交互事件
  5. 注入样式

其中,执行脚本就需要安装每个前端框架的内置方法,将JS代码生成对应的Virtual DOM,然后在通过浏览器内置API将其转换为DOM, 然后才会进行事件的绑定。


SSR

image.png


2. 传统React渲染模式

React 中,视觉更新分为两个阶段:渲染阶段提交阶段

渲染阶段是一个纯计算阶段,其中 React元素与现有的 DOM 进行对比(也就是调和)。这个阶段涉及创建一个新的 React 元素树,也称为虚拟 DOM,它实质上是实际 DOM 的轻量级内存表示

在渲染阶段,React 计算当前 DOM新的 React 组件树之间的差异,并准备必要的更新。

image.png

渲染阶段之后是提交阶段。在这个阶段,React 将在渲染阶段计算得到的更新应用到实际 DOM 上。这涉及创建更新删除 DOM 节点,以反映新的 React 组件树


在传统的同步渲染中,React 对组件树中的所有元素赋予相同的优先级

当组件树被渲染时,无论是在初始渲染还是状态更新时,React 会在一个不可中断的单一任务中渲染整个树,之后将其提交到 DOM 中,以在屏幕上更新组件的可视化效果。

image.png

同步渲染是一种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

image.png

当我们查看性能选项卡时,可以看到每次输入都会发生长时间的任务,这是我们不能容忍的。

image.png


被标记为红色角标的任务被认为是长任务。请注意总阻塞时间为4425.40毫秒

在这种情况下,React 开发者通常会使用像 debounce 这样的第三方库来延迟渲染,但并没有内置的解决方案。


React 18 引入了一个新的并发渲染器,它在后台运行。这个渲染器为我们提供了一些方法来标记某些渲染为非紧急渲染


image.png

当渲染低优先级组件(标记为红色)时,React让出主线程,以便检查是否有更重要的任务需要处理

在这种情况下,React每隔 5 毫秒让出主线程,以查看是否有更重要的任务需要处理,比如用户输入,甚至是渲染其他 React 组件的状态更新,这些任务在当前时刻对用户体验更重要。通过持续地让出主线程,React 能够使这些渲染成为非阻塞的,并优先处理更重要的任务。这样可以改善用户体验并提高应用程序的性能。


image.png

与每次渲染一个单一的不可中断任务不同,新的并发渲染器在渲染低优先级组件时,在每个 5 毫秒的间隔内将控制权交还给主线程

此外,并发渲染器能够在后台“并发”地渲染多个版本的组件树,而不立即提交结果

与同步渲染是一种all-or-nothing的计算方式不同, 并发渲染器允许 React暂停恢复渲染一个或多个组件树,以实现最优化的用户体验。这样的方式可以提高应用程序的性能,并确保用户界面的流畅和响应性。

当某个重要任务出现时,React 可以中断当前的渲染,转而处理该任务,然后在合适的时候继续渲染,避免了阻塞主线程和UI无响应的情况,从而提升了整体的渲染效率。


image.png

React根据用户交互暂停当前的渲染,强制它优先渲染另一个更新

借助并发特性,React 可以根据外部事件(如用户交互)暂停恢复组件的渲染。当用户开始与 ComponentTwo 进行交互时,React 暂停当前的渲染,优先渲染ComponentTwo,然后再继续渲染 ComponentOne

相关文章
|
2月前
|
前端开发 JavaScript UED
使用React Hooks优化前端应用性能
本文将深入探讨如何使用React Hooks来优化前端应用的性能,重点介绍Hooks在状态管理、副作用处理和组件逻辑复用方面的应用。通过本文的指导,读者将了解到如何利用React Hooks提升前端应用的响应速度和用户体验。
|
5月前
|
缓存 前端开发 JavaScript
React 18 如何提升应用性能(二)
React 18 如何提升应用性能(二)
|
4月前
|
缓存 前端开发 JavaScript
React 18 如何提高应用性能?
React 18 如何提高应用性能?
|
10月前
|
缓存 前端开发 JavaScript
React 18 如何提升应用性能
React18 引入了并发功能,从根本上改变了React的渲染方式。一起探索:Concurrent/Transitions API/React Server Components/Suspense 的奥秘
59 0
React 18 如何提升应用性能
|
前端开发 JavaScript 算法
React-利用React-Profiler提升应用性能
React Profiler 的组成 推荐阅读指数 ⭐️⭐️⭐️ 如何通过React Profiler查询并改正页面耗时操作 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
100 0
React-利用React-Profiler提升应用性能
|
4月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
122 0
|
4月前
|
存储 前端开发 JavaScript
【第34期】一文学会React组件传值
【第34期】一文学会React组件传值
33 0
|
4月前
|
前端开发
【第31期】一文学会用React Hooks组件编写组件
【第31期】一文学会用React Hooks组件编写组件
35 0
|
4月前
|
存储 前端开发 JavaScript
【第29期】一文学会用React类组件编写组件
【第29期】一文学会用React类组件编写组件
37 0
|
4月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
43 0