1. 前言

本专题的研究缘起于能源计算器中的性能问题。在一个大组件中,如果包含多个频繁更新的小组件,每次状态变化都会触发整个组件树的重新渲染,导致页面出现卡顿。为解决这一问题,需要使用 React.memouseMemouseCallback 等性能优化手段。它们能够利用 React 渲染机制,通过避免不必要的重新计算和组件更新,减少 Fiber 树的遍历和真实 DOM 的操作,从而提升性能。因此,本专题将深入探究 React 的渲染原理,分析这些优化手段如何在内部机制中发挥作用,以理解性能优化的本质。

2. 基础知识

2.1 浏览器渲染流程

1、DNS解析:浏览器将接收到的域名转换为IP地址找到服务器的位置。

2、建立TCP连接:浏览器与服务器建立TCP连接用于传输数据。

3、发起HTTP请求:浏览器发送HTTP请求到服务器。

4、服务器响应请求并返回数据:服务器处理请求返回请求的HTML、CSS、JS等数据。

5、浏览器解析接收到的数据:浏览器开始解析接收到的HTML代码,并请求HTML中引用的资源文件。

6、构建DOM树:解析HTML代码构建DOM树。这一步的DOM树由React对比新旧树,并进行渲染生成。

7、构建CSSOM树:解析CSS代码构建CSSOM树。

8、构建Render树:将DOM和CSSOM合并,并且生成用于页面显示的Render树。

9、布局Layout:计算Render各个节点中的几何信息。

10、绘制Paint:使用UI后端层绘制各个节点

11、合成Composite:将绘制的各个层级的节点显示在页面上

2.2 React的Render过程

1、调用setstate方法,更新状态

何时会触发setstate事件?

1、事件回调(用户交互)

2、副作用 / 生命周期(如 useEffect、componentDidMount)

3、异步回调(定时器、Promise、WebSocket、事件总线等)

4、props 变化引发的更新

5、手动调用(逻辑里直接 setState)

2、调度阶段(该阶段需要计算出应该更新哪些组件,以及需要哪些DOM操作)

React标记组件为需要重新渲染,标记的类型包括

  • Placement(新增):表示需要在 DOM 中创建新的元素。

  • Update(更新):表示需要更新现有的元素。

  • Deletion(删除):表示需要从 DOM 中删除元素。

根据任务优先级选择 从根节点开始构建 Work-in-Progress 树

可以 中断 / 暂停低优先级任务,让高优先级任务先渲染

3、渲染阶段(该阶段需要将不可停止的大任务拆分为若干小任务,一个任务对应一个节点)x

2.3 React架构

2.3.1 React15架构

React15架构分为协调器和渲染器

2.3.1.1 协调器(Reconciler)

在 React 15.x 里,协调器的核心任务是 从根节点开始递归遍历虚拟 DOM 树,找出需要更新的部分也是需要变化的组件。它的实现是“栈式调用”(递归),所以叫 Stack Reconciler

核心过程

协调器的执行分三步:

递归遍历

生成更新队列

交给渲染器

2.3.1.2 渲染器(Renderer)

Renderer(渲染器)

根据差异,执行具体环境的更新操作(DOM 创建、属性更新、事件绑定、删除节点等)。主要是负责构建和更新DOM树。

Renderer 负责将变化的组件渲染到页面上,不同的平台有不同的渲染器

react15的渲染器包含下面几个Renderer

2.3.2 React 16之后的架构

React16 架构分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler

  • Reconciler(协调器)—— 负责找出变化的组件

  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

与 React15 相比,就多了 Scheduler(调度器),正是因为调度器,才使得 React 有了能够中断任务切片、调整优先级等能力

2.3.2.1 调度器(Scheduler)

react16将更新任务进行拆分,通过调度器安排任务的执行

执行过程

2.3.2.2 协调器(Reconciler)

react16的协调器相比于react15进行了改进,引入了 Fiber 架构,把每个虚拟 DOM 元素对应为一个 Fiber 节点。Fiber 节点通过 childsiblingreturn 指针组成一种 双向链表树结构,使得更新过程可以被中断、恢复,并支持不同优先级的任务调度。

执行过程

2.3.2.3 渲染器(Renderer)

React 16 的协调器(Fiber Reconciler)会生成 副作用链表(effect list),标记哪些节点需要 插入、更新、删除。渲染器的主要任务就是 执行这些副作用,把 UI 更新到最终呈现的地方。

执行过程:

2.3.2.4 Fiber架构与双缓存

React的Fiber

Fiber 把一个渲染任务分解为多个渲染任务,而不是一次性完成,把每一个分割得很细的任务视作一个”执行单元”,React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去,故任务会被分散到多个帧里面,中间可以返回至主进程控制执行其他任务,最终实现更流畅的用户体验。

即是实现了”增量渲染”,实现了可中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber 节点。

Fiber节点结构大致如下:

Fiber 架构可以分为三层:

  • Scheduler 调度器 —— 调度任务的优先级,高优任务优先进入 Reconciler

  • Reconciler 协调器 —— 负责找出变化的组件

  • Renderer 渲染器 —— 负责将变化的组件渲染到页面上

相比 React15,React16 多了Scheduler(调度器),调度器的作用是调度更新的优先级。

在新的架构模式下,工作流如下:

  • 每个更新任务都会被赋予一个优先级。

  • 当更新任务抵达调度器时,高优先级的更新任务(记为 A)会更快地被调度进 Reconciler 层;

  • 此时若有新的更新任务(记为 B)抵达调度器,调度器会检查它的优先级,若发现 B 的优先级高于当前任务 A,那么当前处于 Reconciler 层的 A 任务就会被中断,调度器会将 B 任务推入 Reconciler 层。

  • 当 B 任务完成渲染后,新一轮的调度开始,之前被中断的 A 任务将会被重新推入 Reconciler 层,继续它的渲染之旅,即“可恢复”。

双缓存

react内部有两颗树维护着两个状态:一个是fiber tree,一个是work in progress fiber tree

  1. fiber tree:表示当前正在渲染的fiber树

  2. work in progress fiber tree:表示更新过程中新生成的fiber树,也就是渲染的下一次UI状态

举个例子:

当我们用 canvas 绘制动画时,每一帧绘制前都会调用 ctx.clearRect 清除上一帧的画面,如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。

为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。

2.4 React diff算法

react中diff算法主要遵循三个层级策略:Tree层级、component层级、Element层级。

  1. Tree 层级

在树的层级比较中,React 不会对跨层级的 DOM 节点进行优化,仅在同一层级的节点之间进行比对。

  • 当旧树中某节点在新树中不存在时,直接 删除旧节点,在目标父节点下 创建新节点及其子节点

  • 此阶段操作仅涉及 删除和创建,不会进行节点移动。

例如,如果新树中 R 节点下没有 A,则 React 会直接删除旧树中的 A,然后在 D 节点下重新创建 A 及其子节点。

  • Component 层级

在组件层级的 diff 过程中:

  • 同类组件(即同一个类/函数组件):继续递归 diff 子节点。

  • 不同组件:直接删除旧组件及其子节点,创建新组件及其子节点。

例如,如果 Component D 被替换成 Component G,即使两者结构非常相似,React 也会删除 D 并创建新的 G

  • Element 层级(同层级节点集合)

在同一层级的节点集合中,每个节点通过唯一的 key 进行标识,提供三种操作类型:

通过 key,React 可以识别新旧集合中相同节点,从而避免不必要的删除和创建,仅执行必要的移动操作。

移动节点的 diff 流程

假设有新集合和旧集合节点如下:

  • index:新集合遍历下标

  • oldIndex:当前节点在旧集合中的下标

  • maxIndex:当前已访问的新集合节点在旧集合中的最大下标

规则如下:

  1. oldIndex > maxIndex → 不移动节点,并更新 maxIndex = max(oldIndex, maxIndex)

  2. oldIndex = maxIndex → 不操作

  3. oldIndex < maxIndex → 将节点移动到当前 index 位置

流程示例(节点顺序:A, B, C, D)

  1. 节点 BmaxIndex = 0oldIndex = 1 → 1 > 0,不移动,更新 maxIndex = 1

  2. 节点 AmaxIndex = 1oldIndex = 0 → 0 < 1,需要移动

  3. 节点 DmaxIndex = 1oldIndex = 3 → 3 > 1,不移动,更新 maxIndex = 3

  4. 节点 CmaxIndex = 3oldIndex = 2 → 2 < 3,需要移动

最后,diff 完成后还会遍历旧集合,删除未被使用的节点。

算法示意图

3. React渲染流程

  • 初始化阶段

  • 任务调度(Scheduler)

  • Render 阶段(协调器 Reconciler)

  • Commit 阶段(渲染器 Renderer)
    Commit 阶段会遍历 Effect List,执行相应副作用,并触发生命周期方法(如 componentDidMountcomponentDidUpdatecomponentWillUnmount)。不同环境有不同的渲染器:在浏览器中是 ReactDOM,在 Canvas 或 SVG 中是 ReactART 等。Commit 阶段负责将内存中的计算结果应用到真实 UI 上,保证界面与 Fiber 树状态一致。

3.1 构建阶段

首先,JSX 会经过 Babel 的 AST 解析和编译,转换为 React.createElement 调用。

3.2 触发更新

用户操作或状态更新(setStatedispatch)触发 React 更新。React执行 React.createElement ,之后会返回一个 JavaScript 对象,用于描述 UI 结构,这个对象就是我们常说的 虚拟 DOM(Virtual DOM)

3.3 任务调度

React 会将每次更新封装成一个 更新任务(update task),其中包含要更新的组件、更新内容以及任务的优先级信息。无论是首次渲染还是状态更新,这些任务都会经过 Scheduler 调度。Scheduler 会根据任务的优先级决定哪些任务先进入 Render 阶段。例如,用户触发的更新优先级很高,如果当前正在执行一个耗时任务,Scheduler 会打断该任务,优先处理用户更新。

在任务初始化时,Scheduler 会为不同类型的任务计算 过期时间:优先级越高的任务过期时间越短,优先级低的任务过期时间越长。Scheduler 还会为任务分配 时间片,如果任务在一个时间片内未完成,会暂停当前 Fiber 节点的计算,将执行权交给浏览器,待浏览器空闲时再从暂停的 Fiber 节点继续计算。

需要注意的是,Scheduler 仅负责 调度任务,实际的 Fiber 差异计算和副作用标记则由 Render 阶段的 Reconciler 完成。

3.4 Render阶段

Render 阶段的核心是 Reconciler。就是构建和协调一棵新的 Fiber 树,然后在 commit 阶段把变化同步到 DOM。

执行过程

协调器(Fiber Reconciler)遍历 Fiber 树,对比 新旧虚拟 DOM

Diff 算法计算最小更新集:

  • Tree 层级:跨层级节点不做移动优化,仅删除/创建。

  • Component 层级:同类组件递归 diff,不同组件直接删除再创建。

  • Element 层级:同层级节点通过 key 匹配,支持插入、移动、删除。

协调器生成 effect list,记录每个节点需要的操作(Placement、Update、Deletion)。

这一阶段可以 中断,调度器可以暂停任务,让浏览器处理高优先级任务(如用户输入)。

3.5 commit阶段

渲染器按照 effect list 顺序执行操作,将更新应用到真实 DOM 或原生控件:

  • 删除节点 → 插入新节点 → 更新属性和事件。

同时触发生命周期方法:componentDidMountcomponentDidUpdatecomponentWillUnmount

这一阶段 不可中断,保证 UI 与协调器状态一致。

4. React渲染性能优化方式及原理

❓引起组件重新渲染的因素有哪些?

  1. State 更新:组件自身的 state 发生变化时,会触发重新渲染。

  2. Props 变化:组件接收到新的 props 时,如果值发生改变,会重新渲染。

  3. 父组件重新渲染:父组件每次渲染都会默认触发子组件渲染,除非使用 React.memo 或其他优化手段。

  4. Context 更新:当组件订阅的 Context 值发生变化时,会导致组件重新渲染。

  5. Force Update:通过 forceUpdate() 强制触发渲染。

  6. 父组件传递的函数或对象引用变化:每次渲染创建新的函数或对象引用,会被视为 props 变化,从而触发子组件重新渲染。

🌟下面是基于Reacr渲染原理的性能优化方法

组件级优化

  1. React.memo

    • 适用于函数组件,避免 props 未变化时的重复渲染。

    • 例如列表项组件,父组件频繁渲染时可用 memo 缓存子组件。PureComponent(类组件):对 state 和 props 做浅比较,避免不必要的渲染。

  2. 拆分组件(Component Splitting)

    • 将大组件拆成多个小组件,减少一次性重渲染范围。

Hook与计算缓存优化

  1. useMemo

    • 对复杂计算结果进行缓存,只有依赖项变化时才重新计算。

    • 适合 CPU 密集型计算或数据处理逻辑。

  2. useCallback

    • 缓存函数引用,避免因函数地址变化导致子组件不必要的渲染。

    • 常用于 onClick / onChange 等事件回调。

列表渲染优化

key 的使用:确保列表 diff 算法正确匹配节点,避免重复渲染。

虚拟列表(Virtualized List)

问题背景

在列表渲染过程中

如果列表的数据有几千甚至上万条数据,会导致页面卡顿,首次渲染慢,滚动体验差。

只渲染可视区域的列表项,适合大数据列表。

状态管理优化

  • 拆分组件状态:将局部状态拆到最小组件,减少状态更新影响范围。

  • 使用 useReducer 或状态管理库:减少不必要的全局组件更新。

渲染调度与异步优化

  • React Concurrent Mode / Suspense:分片渲染和异步任务调度,提升响应速度。

  • 批量更新:React 默认批量处理多次 state 更新,减少重复渲染。

避免不必要的副作用

  • useEffect/useLayoutEffect 优化依赖:只在依赖变化时执行副作用,避免重复执行。