# react性能瓶颈
我们日常使用App,浏览网页时,有两类场景会制约快速响应
- CPU瓶颈:当遇到大计算量的操作或者设备性能不足使页面掉帧,导致卡顿
- IO瓶颈:发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应;我们可以通过加入loading来解决体验问题, 为此,React实现了Suspense (opens new window)功能及配套的hook——useDeferredValue
当项目变得庞大、组件数量繁多时,就容易遇到CPU的瓶颈,考虑如下Demo,向视图中渲染3000个li
function App() {
const len = 3000;
return (
<ul>
{Array(len).fill(0).map((_, i) => <li>{i}</li>)}
</ul>
);
}
const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);
主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次。我们知道,JS可以操作DOM,GUI渲染线程与JS线程 是互斥的。所以JS脚本执行和浏览器布局、绘制不能同时执行,在每16.6ms时间内,需要完成如下工作:
JS脚本执行 ----- 样式布局 ----- 样式绘制
当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局和样式绘制了。在Demo中,由于组件数量繁多(3000个),JS
脚本执行时间过长,页面掉帧,造成卡顿。可以从打印的执行堆栈图看到,JS执行时间为73.65ms,远远多于一帧的时间
react解决这个问题是通过,在浏览器每一帧的时间中,预留一些时间给JS线程,React利用这部分时间更新组件(源码中可查预留的
初始时间是5ms)当预留的时间不够用时,React将线程控制权交还给浏览器使其有时间渲染UI,React则等待下一帧时间到来继续被中
断的工作。总之就是:将同步的更新变为可中断的异步更新。
这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)
接下来我们开启Concurrent Mode(后续章节会讲到,当前你只需了解开启后会启用时间切片)
// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(<App/>, rootEl);
ReactDOM.unstable_createRoot(rootEl).render(<App/>);
此时我们的长任务被拆分到每一帧不同的task中,JS脚本执行时间大体在5ms左右,这样浏览器就有剩余时间执行样式布局和样式绘制,减少掉帧的可能性

# React15架构
React15架构可以分为两层:
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
我们知道,在React中可以通过this.setState、this.forceUpdate、ReactDOM.render等API触发更新,每当有更新发生 时,Reconciler会做如下工作
- 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
- 将虚拟DOM和上次更新时的虚拟DOM对比
- 通过对比找出本次更新中变化的虚拟DOM
- 通知Renderer将变化的虚拟DOM渲染到页面上
由于React支持跨平台,所以不同平台有不同的Renderer。我们前端最熟悉的是负责在浏览器环境渲染的Renderer —— ReactDOM 除此之外,还有:
- ReactNative (opens new window)渲染器,渲染App原生组件
- ReactTest (opens new window)渲染器,渲染出纯Js对象用于测试
- ReactArt (opens new window)渲染器,渲染到Canvas, SVG 或 VML (IE8) 在每次更新发生时,Renderer接到Reconciler通知,将变化的组件渲染在当前宿主环境
# React15架构的缺点
在Reconciler中,mount的组件会调用mountComponent (opens new window),update的组件会调用 updateComponent (opens new window)。这两个方法都会递归更新子组件,由于递归执行,所以更新一旦开始,中途就无法 中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。在上一节中,我们已经提出了解决办法——用可中断的异步更 新代替同步的更新,我们看下下面这个例子:
// 页面 // 页面改变之后
<li>1</li> <li>2</li>
<li>2</li> ====> <li>4</li>
<li>3</li> <li>6</li>
- 点击button,state.count从1变成2
- Reconciler发现1需要变为2,通知Renderer
- Renderer更新DOM,1变为2
- Reconciler发现2需要变为4,通知Renderer
- Renderer更新DOM,2变为4
- Reconciler发现3需要变为6,通知Renderer
- Renderer更新DOM,3变为6 我们可以看到,Reconciler和Renderer是交替工作的,当第一个li在页面上已经变化后,第二个li再进入Reconciler,由于整个 过程都是同步的,所以在用户看来所有DOM是同时更新的
接下来,让我们模拟一下,如果中途中断更新会怎么样?
- 点击button,state.count从1变成2
- Reconciler发现1需要变为2,通知Renderer
- Renderer更新DOM,1变为2;后续的4、5、6、7步骤被中断 当第一个li完成更新时中断更新,即步骤3完成后中断更新,此时后面的步骤都还未执行。用户本来期望123变为246。实际却看见更 新不完全的DOM!(即223)基于这个原因,React决定重写整个架构
# React16架构
React16架构可以分为三层:
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上 可以看到,相较于React15,React16中新增了Scheduler(调度器),让我们来了解下他
# Scheduler(调度器)
既然我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。其实部分浏览器已经 实现了这个API,这就是requestIdleCallback (opens new window)。但是由于以下因素,React放弃使用:
- 浏览器兼容性
- 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低 基于以上原因,React实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外, Scheduler还提供了多种调度优先级供任务设置
# Reconciler(协调器)
我们知道,在React15中Reconciler是递归处理虚拟DOM的。让我们看看React16的Reconciler (opens new window)。 我们可以看见,更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间。
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
在React16中,Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟 DOM打上代表增/删/更新的标记,类似这样:
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
# Renderer(渲染器)
Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作,在React16架构中整个更新流程为
其中红框中的步骤随时可能由于以下原因被中断:
- 有其他更高优任务需要先更新
- 当前帧没有剩余时间 由于红框中的工作都在内存中进行,不会更新页面上的DOM,所以即使反复中断,用户也不会看见更新不完全的DOM
# Fiber架构
首先我们得知道代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离
代数效应与React关系最明显的例子就是Hooks,对于类似useState、useReducer、useRef这样的Hook,我们不需要关注 FunctionComponent的state在Hook中是如何保存的,React会为我们处理。我们只需要假设useState返回的是我们想要的 state,并编写业务逻辑就行
function App() {
const [num, updateNum] = useState(0);
return (
<button onClick={() => updateNum(num => num + 1)}>{num}</button>
)
}
从React15到React16,协调器(Reconciler)重构的一大目的是:将老的同步更新的架构变为异步可中断更新。异步可中断更新可 以理解为:更新在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态, 其实,浏览器原生就支持类似的实现,这就是Generator,但是Generator的一些缺陷使React团队放弃了他:
- 类似async,Generator也是传染性的,使用了Generator则上下文的其他函数也需要作出改变。这样心智负担比较重
- Generator执行的中间状态是上下文关联的
function* doWork(A, B, C) {
var x = doExpensiveWorkA(A);
yield;
var y = x + doExpensiveWorkB(B);
yield;
var z = y + doExpensiveWorkC(C);
return z;
}
- 每当浏览器有空闲时间都会依次执行其中一个doExpensiveWork,当时间用尽则会中断,当再次恢复时会从中断位置继续执行。
- 只考虑“单一优先级任务的中断与继续”情况下Generator可以很好的实现异步可中断更新。
- 但是当我们考虑“高优先级任务插队”的情况,如果此时已经完成doExpensiveWorkA与doExpensiveWorkB计算出x与y。时B组件接 收到一个高优更新,由于Generator执行的中间状态是上下文关联的,所以计算y时无法复用之前已经计算出的x,需要重新计算。如果 通过全局变量保存之前执行的中间状态,又会引入新的复杂度,所以react没有使用Generator
# Fiber架构实现原理
在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多 时间,造成卡顿。为了解决这个问题,React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结 构已经无法满足需要,于是,全新的Fiber架构应运而生。
Fiber包含三层含义:
作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。
作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)
Fiber的结构
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
// 每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性
this.return = null; // 指向父级Fiber节点
this.child = null; // 指向子Fiber节点
this.sibling = null; // 指向右边第一个兄弟Fiber节点
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
举个例子,如下代码会生成这个结构
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
)
}

这里需要提一下,为什么父级指针叫做return而不是parent或者father呢?因为作为一个工作单元,return指节点执行完 completeWork(本章后面会介绍)后会返回的下一个节点。子Fiber节点及其兄弟节点完成工作后会返回其父级节点,所以用return指代父级节点
# Fiber架构工作原理
首先我们来了解下什么叫双缓存?
当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。如果当前帧画面计算量比较大,导致清除上一帧 画面到绘制当前帧画面之间有较长间隙,就会出现白屏。为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前 帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。这种在内存中构建并直接替换的技术叫 做双缓存 (opens new window);React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新
双缓存Fiber树在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存 中构建的Fiber树称为workInProgress Fiber树。
current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React应用的根节点通过current指针在不同Fiber树的rootFiber间切换来实现Fiber树的切换。当workInProgress Fiber树构 建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber 树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完 成DOM更新。
接下来我们以具体例子讲解mount时、update时的构建/替换流程
function App() {
const [num, add] = useState(0);
return (
<p onClick={() => add(num + 1)}>{num}</p>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
- 首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节
点,rootFiber是
所在组件树的根节点。之所以要区分fiberRootNode与rootFiber,是因为在应用中我们可以多次调用 ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode。
fiberRootNode的current会指向当前页面上已渲染内容对应对Fiber树,被称为current Fiber树
fiberRootNode.current = rootFiber;
由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)
- 接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。(下图中右侧为内存中构建的树,左侧为页面显示的树)
在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)

- 图中右侧已构建完的workInProgress Fiber树在commit阶段渲染到页面
此时DOM更新为右侧树对应的样子。fiberRootNode的current指针指向workInProgress Fiber树使其变为current Fiber 树

那么当我们update的时候流程如下
- 接下来我们点击p节点触发状态改变,这会开启一次新的render阶段并构建一棵新的workInProgress Fiber 树
和mount时一样,workInProgress fiber的创建可以复用current Fiber树对应的节点数据
这个决定是否复用的过程就是Diff算法,后面章节会详细讲解
- workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树
