React渲染机制及相关优化方案
- 前言:
- 一、react渲染步骤
- 二、concurrent机制以及产生作用的机会
- 1. 优先级调度:
- 2. 递增式渲染:
- 三、简单模拟实现 concurrent mode 的递增式渲染
- 四、与优先级调度有关的两个hooks
- 1. useTransition
- 2. useDeferredValue
- 3. useTransition 与 useDeferredValue 的区别
- 4. 应用场景
- 五、一个小例子
- 1. 下面使用 useTransition 进行优化
- 2. 使用 useDeferredValue 进行优化
- 补充:为什么VUE不需要设计 Concurrent Mode
前言:
- 文章主要简单记录react中的渲染机制以及相关的优化方案,内容包括如下:
- react渲染步骤、concurrent机制以及产生作用的机会
- 简单模拟实现 concurrent mode
- 基于作业调度优先级的思路进行项目优化的两个hooks
- 案例演示
一、react渲染步骤
-
准备阶段(Prepare Phase)
在准备阶段,React 会收集组件的依赖关系,建立组件树的数据结构,确定组件的更新优先级,并生成用于渲染的工作单元。 -
计算阶段(Compute Phase)
在计算阶段,React 会根据组件的更新优先级和调度策略,将工作单元分成多个批次进行处理。每个批次都会执行一小部分工作单元,以保证用户界面的响应性。 -
渲染阶段(Render Phase)
在渲染阶段,React 会根据工作单元的类型和优先级,执行相应的渲染操作。这包括创建新的虚拟 DOM 节点、更新现有的虚拟 DOM 节点,以及卸载不再需要的组件。 -
提交阶段(Commit Phase)
在提交阶段,React 会将更新后的虚拟 DOM 节点映射到实际的 DOM,更新用户界面。这个阶段还会执行一些副作用操作,如执行useEffect。
二、concurrent机制以及产生作用的机会
注:React 的并发模式(Concurrency Mode)是一种用于处理大型和复杂应用程序的特性,旨在提高应用程序的性能和响应能力。解决react中状态更新就会触发该组件及该组件下所有子组件无脑更新而引发的性能问题;同时提供部分控制作业调度优先级的能力给开发者使用
-
在传统的 React 渲染模式中,更新操作是同步进行的,即在进行更新时,会立即进行组件的重新渲染,可能会阻塞主线程,导致页面响应变慢或失去响应出现掉帧问题。
-
而concurrent mode通过引入一种新的调度算法和优先级机制,将更新操作划分为多个优先级,使得 React 可以更好地管理和分配任务,以实现更平滑的用户体验。
-
concurrent mode主要具备以下几个特性:异步渲染、优先级调度、递增式渲染
补充:concurrent mode 主要工作在渲染流程的 Compute Phase 及 Render Phase,因为它们是纯粹的 JS 计算意味着可以被拆分,而 commit 阶段由于带有 DOM 更新,不可能 DOM 变更到一半中断,因此必须一次性执行完成
1. 优先级调度:
concurrent mode 通过对任务进行优先级划分,React 可以根据优先级动态地分配和重新分配任务。基于此React 可以更好地响应用户交互和其他高优先级的任务,同时提供了 “useDeferredValue” 、“useTransition” 两个hooks用于调度作业任务的优先级。
2. 递增式渲染:
1)concurrent mode 下的渲染是逐步进行的,React 将大量需要重新渲染的组件的工作基于时间片的理念划分为多个小片段工作,在浏览器的每一帧的空闲时间中去执行这些渲染工作,而不是一下子全部直接执行,这样有效的避免了掉帧情况的出现。
2)这里也就说明了为什么React官方说 componentWillMount 可能被调用多次的原因,正是因为低优先级任务的 render 阶段可能被重复的中断和重新执行,而 componentWillMount 就包含在 render 阶段中。
注意:工作拆分的最小单元应该是一个组件,当某个组件本身的计算就十分巨大时依然会导致卡帧,不过我们可以通过调整工作的优先级使得用户的体验是平滑的
三、简单模拟实现 concurrent mode 的递增式渲染
- 下面使用 requestIdleCallback 函数模拟时间片,在每一帧的空闲时间进行js计算从而达到递增式渲染的效果
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="index.js"></script>
</head>
<body>
<div id="root"></div>
<script>
// 调用render提供挂载容器 "root"
render(document.getElementById('root'))
</script>
</body>
</html>
index.js
// 页面需要渲染的组件
function Counter() {
return {
type: 'span',
value: 'hello world',
next: {
type: 'p',
value: 'hello LiHua'
}
}
}
const CounterElementDescriptors = {
type: 'Function',
fn: Counter
}
// 记录当前工作
let presentWork = null
// 记录根元素
let rootElementDescriptore = null
// 记录挂载容器
let elementsContainer = null
// 处理单元任务
function performUnitOfWork(deadline) {
// 判断当前是否还有待执行任务
if (presentWork == null) return commitRoot(rootElementDescriptore)
// 当前帧超时,调用 requestIdleCallback 把任务推到下一帧空闲时间执行
if (deadline.didTimeout) return requestIdleCallback(executeWorkLoop)
// 若是组件则处理依赖关系、若是元素则生成真实dom
if (presentWork.type === "Function") {
rootElementDescriptore = presentWork
const firstChildren = presentWork.fn()
firstChildren.parent = presentWork
presentWork.children = firstChildren
presentWork = firstChildren
performUnitOfWork(deadline)
} else {
const dom = document.createElement(presentWork.type)
dom.innerHTML = presentWork.value
presentWork.dom = dom
presentWork = presentWork.next
performUnitOfWork(deadline)
}
}
// 控制循环执行工作
function executeWorkLoop(deadline) {
performUnitOfWork(deadline)
}
// 提供render函数,用于获取挂载容器和开始渲染计算工作
function render(element) {
elementsContainer = element
presentWork = CounterElementDescriptors
requestIdleCallback(executeWorkLoop)
}
// 模拟commit阶段
function commitRoot(rootElement) {
let renderCHildrenElements = rootElement.children
do {
elementsContainer.appendChild(renderCHildrenElements.dom)
renderCHildrenElements = renderCHildrenElements.next
}while(renderCHildrenElements)
}
四、与优先级调度有关的两个hooks
1. useTransition
官方解释:useTransition 是一个让你在不阻塞 UI 的情况下来更新状态的 React Hook。
- 通过 useTransition 我们可以将一部分的状态更新工作划分为低优先级的异步任务,使它不阻塞主要任务的执行
- 同时我们可以依据 useTransition 返回的标志状态在渲染期间优雅地展示加载状态,从而提高用户界面的交互体验和流畅性
- useTransition 主要语法如下:
import { useTransition } from "react";
function TabContainer() {
// isPending 标志,告诉你是否存在待处理的低优先级工作。
// startTransition 函数 允许你将该部分的状态更新标记为低优先级。
const [isPending, startTransition] = useTransition();
function handle() {
startTransition(() => {
// 低优先级的状态更新工作
{......}
});
}
return (
{......}
)
}
2. useDeferredValue
官方解释:useDeferredValue 是一个 React Hook,可以让你延迟更新 UI 的某些部分。
- 通过 useDeferredValue 我们可以将一部分的UI更新工作划分为低优先级的任务,使它不阻塞主要任务的执行
- useTransition 主要语法如下:
import { useDeferredValue, useState, } from "react";
function TabContainer() {
const [query, setQuery] = useState('');
// 定义的 deferredQuery 获取的是query的延迟版本
const deferredQuery = useDeferredValue(query);
function handle(data) {
setQuery(data)
}
return (
{ ......}
)
}
3. useTransition 与 useDeferredValue 的区别
- useTransition 用于控制过渡状态,可以在过渡状态中执行任务,并提供过渡状态的布尔值来判断是否处于过渡状态。
- useDeferredValue 用于延迟某个值的更新,以避免在渲染过程中处理昂贵的计算或数据获取,确保界面的流畅性。
- 虽然它们都与并发模式相关,但用途和作用略有不同,具体使用哪一个需要看具体场景。
4. 应用场景
1)长列表渲染:当渲染大量列表项时,可以对列表项的渲染任务调节为低优先级异步任务,以保证用户界面的响应性能。
2)大型表单处理:对于包含大量输入字段的表单,可以使用合理使用对于hooks将表单提交和验证等任务进行优化调节,以避免阻塞用户界面。
3)图片懒加载:当页面中包含大量图片时,可以使用 useTransition 将图片的加载划分为多个低优先级异步任务,在渲染期间逐步加载图片,以减少对用户界面的阻塞。
4)异步数据加载:当页面中的数据需要从后端异步加载时,可以使用 useTransition 将数据的加载划分为多个异步任务,以保证用户界面的响应性能。
五、一个小例子
- 以下以长列表渲染为例子做演示
基础代码,未作优化处理:
import React, { useCallback, useState } from 'react'
const index: React.FC = () => {
const [list, setList] = useState<any[]>([])
const handleSearch = useCallback((value: string) => {
const newList = []
for (let i = 0; i < 5000; i++) {
newList.push(value + '-' + i)
}
setList(newList)
}, [])
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} type='text' />
<div>
{list.map(item => <div key={item}>数据项:{item}</div>)}
</div>
</>
)
}
export default index
当我们进行持续的输入时是十分的卡顿的,效果如下:
1. 下面使用 useTransition 进行优化
- 降低 “setList(newList)” 的优先级,使其不阻塞用户输入事件的触发
代码修改如下:
import React, { useCallback, useState, useTransition } from 'react'
const index: React.FC = () => {
const [list, setList] = useState<any[]>([])
const [isPending, startTransition] = useTransition()
const handleSearch = useCallback((value: string) => {
startTransition(() => {
const newList = []
for (let i = 0; i < 5000; i++) {
newList.push(value + '-' + i)
}
setList(newList)
})
}, [])
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} type='text' />
<div>
{isPending? '加载中。。。' : list.map(item => <div key={item}>数据项:{item}</div>)}
</div>
</>
)
}
export default index
优化后效果如下:
2. 使用 useDeferredValue 进行优化
- 降低 “列表部分UI” 更新渲染的优先级,使其不阻塞用户输入事件的触发
代码修改如下:
import React, { useCallback, useDeferredValue, useMemo, useState, useTransition } from 'react'
const List = ({ listData }: { listData: string[] }) => {
return (
<>
{listData.map(item => <div key={item}>数据项:{item}</div>)}
</>
)
}
const index: React.FC = () => {
const [list, setList] = useState<any[]>([])
const deferredList = useDeferredValue(list)
const handleSearch = useCallback((value: string) => {
const newList = []
for (let i = 0; i < 5000; i++) {
newList.push(value + '-' + i)
}
setList(newList)
}, [])
return (
<>
<input onChange={(e) => handleSearch(e.target.value)} type='text' />
<List listData={deferredList} />
</>
)
}
export default index
优化后效果如下:
补充:为什么VUE不需要设计 Concurrent Mode
- 出于vue响应式系统的设计实现思路的不同,也就体现了为什么。
1)在vue中,响应式系统通过 proxy 实现对 render函数 的依赖收集和触发更新,基于追踪组件依赖的响应式数据的变化,可以更为精准的实现组件的更新,大大避免了不必要的渲染和更新操作,规避了react中状态更新就会触发组件及该组件下所有子组件无脑更新的问题。
2)同时vue的异步更新策略也有助于提高性能和响应能力。Vue会在下一个事件循环周期中批量更新组件,这样可以避免频繁的DOM操作和重复渲染,提高渲染效率。
3)但vue中暂时没有 useTransition 和 useDeferredValue 类似的功能操作,无法调度控制作业的优先级
提示:文章到此结束,文章为个人学习记录,侵删。