面试题:是否了解 React 的内部架构?Fiber 相较于旧的 Stack 架构,有哪些核心优势?
常见的回答方式是:
Stack 架构是采用递归,进行虚拟 DOM 树的比较,计算会消耗大量的时间,新的 Fiber 架构采用的是链表,可以实现时间切片(time slice),防止 JS 的计算占用过多的时间从而导致浏览器出现丢帧的现象。
React v15以及之前的架构称之为 Stack Reconciler
架构,从 v16 开始,React 重构了整体的架构,新的架构被称之为 Fiber
架构,新的架构相比旧架构有一个最大的特点就是能够实现时间切片(time slice)
。
🌩️ 1. Stack架构的问题
之前使用的 Stack
架构,在当时主要关注「声明式 UI」「高效 diff」。然而随着我们业务越来越复杂、交互越来越丰富,性能瓶颈逐渐暴露出来。
总结起来实际上有两大类场景
-
当你需要执行大量计算或者设备本身的性能不足的时候,页面就会出现掉帧、卡顿的现象,这个本质上是来自于 CPU 的瓶颈
-
进行 I/O 的时候,需要等待数据返回后再进行后续操作,等待的过程中无法快速响应,这种情况实际上是来自于 I/O 的瓶颈
🎨(1)CPU 瓶颈
平时我们浏览的网页,实际上是由浏览器绘制出来的,就像一个画家画画一样。

所浏览的网页,往往会有一些动起来的元素,比如轮播图、动画之类的,本质其实就是浏览器不停的在进行绘制。
目前,大多数设备的刷新频率为 60 FPS
,意味着 1秒钟需要绘制 60 次
,1000ms / 60 = 16.66ms,也就是说浏览器每隔 16.66ms
就需要绘制一帧。
浏览器在绘制一帧画面的时候,还有很多的事情要做:

上图中的任务被称之为渲染流水线
,每次执行流水线的时候,大致是需要如上的一些步骤,但是并不是说每一次所有的任务都需要全部执行:
- 当通过 JS 或者 CSS 修改 DOM 元素的几何属性(比如长度、宽度)时,会触发完整的渲染流水线 ,这种情况称之为
重排(回流)
- 当修改的属性不涉及几何属性(比如字体、颜色)时,会省略掉流水线中的 Layout、Layer 过程,这种情况称之为
重绘
- 当修改"不涉及重排、重绘的属性(比如 transform 属性)"时,会省略流水线中 Layout、Layer、Print 过程,仅执行合成线程的绘制工作,这种情况称之为
合成
按照性能高低进行排序的话:合成 > 重绘 > 重排
前面提到浏览器绘制的频率是 16.66ms
一帧,但是执行 JS 与渲染流水线实际上是在同一个主线程上执行,也就意味着如果 JS 执行的时间过长,不能够及时的渲染下一帧,也就意味着页面掉帧,呈现给用户的就是卡顿。
html
<style>
.container {
position: relative;
}
.bar {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background-color: red;
animation: move 4s infinite;
}
@keyframes move {
0% {
left: 0;
}
100% {
left: 500px;
}
}
</style>
<body>
<div class="container">
<button class="btn">延时</button>
<div class="bar"></div>
</div>
<script>
const btn = document.querySelector('.btn');
function delay(duration) {
var time = Date.now();
while(Date.now() - time < duration) {
}
}
btn.addEventListener('click', ()=> {
delay(3000);
})
</script>
</body>
上述代码表现,方块在执行动画的过程中,点击延时按钮,呈现出来的效果是,动画暂定了,延时结束后,方块进行瞬移,这个现象就是我们上述说提到的掉帧
。
在 Reactv16 之前就存在这个问题,JS 代码执行的时间过长。在 React 中,需要去计算整颗虚拟 DOM 树,虽然说是 JS 层面的计算,相比直接操作 DOM,节省了很多时间,但是每次重新去计算整颗虚拟 DOM 树,会造成每一帧的 JS 代码的执行时间过长,从而导致动画、还有一些实时更新得不到及时的响应,造成卡顿的视觉效果。
假设有如下的 DOM 层次结构:
那么转换成虚拟 DOM 对象结构大致如下:
js
{
type : "div",
props : {
id : "test",
children : [
{
type : "h1",
props : {
children : "This is a h1"
}
}
{
type : "p",
props : {
children : "This is a p"
}
},{
type : "ul",
props : {
children : [{
type : "li",
props : {
children : "red"
}
},{
type : "li",
props : {
children : "green"
}
},
{
type : "li",
props : {
children : "yellow"
}
}]
}
}
]
}
}
在 React v16 版本之前,进行两颗虚拟 DOM 树的对比的时候,需要涉及到遍历上面的结构,这个时候只能使用递归,而且这种递归是不能够打断的,一条路走到黑,从而造成了 JS 执行时间过长。
这样的架构模式,官方就称之为 Stack 架构模式,因为采用的是递归,会不停的开启新的函数栈。
🌐(2)I/O瓶颈
前端最典型的 I/O 瓶颈就是网络延迟(比如等待接口返回)。但更大的问题在于用户对不同场景的「延迟容忍度」不同:
- 输入框输入稍微卡一下就难以接受。
- 大数据表格加载loading好几秒,用户可能觉得正常。
Stack
架构中并不会区别对待:所有更新优先级一致,先触发的先执行,无法区分用户交互的紧急程度。这就导致了高优先级的更新(用户交互:输入框输入),有可能被低优先级的更新阻塞,导致用户体验降低。
对于 React
来讲,所有的操作都是来自于自变量的变化导致的重新渲染,我们只需要针对不同的操作赋予不同的优先级即可。
具体来说,主要包含以下三个点:
- 为不同操作造成的"自变量变化"赋予不同的优先级
- 所有优先级统一调度,优先处理"最高优先级的更新"
- 如果更新正在进行(进入虚拟 DOM 相关工作),此时有"更高优先级的更新"产生的话,中段当前的更新,优先处理高优先级更新
要实现上面的这三个点,就需要 React 底层能实现:
- 用于调度优先级的调度器
- 调度器对应的调度算法
- 支持可中断的虚拟 DOM 的实现
所以不管是解决 CPU 的瓶颈还是 I/O 的瓶颈,底层的诉求都是需要实现时间切片(time slice)
,也就是说将较大的渲染任务分解为多个较小的片段,每个片段都可以在一帧内完成,防止长时间的任务阻塞主线程,保持页面流畅性。
🔥 2. Fiber 架构的革新:可中断、可抢占
👀(1)解决 CPU 瓶颈
从 React v16 开始,官方团队正式引用了 Fiber
的概念,这是一种通过链表来描述 UI 的方式,本质上你也可以看作是一种虚拟 DOM 的实现。
Fiber 本质上也是一个对象,但是和之前 React 元素不同的地方在于对象之间使用链表的结构串联起来,child
指向子元素,sibling
指向兄弟元素,return
指向父元素。
如下图:
这种链表结构就意味着:
- 不需要递归了,可以用
while
遍历。 - 任何时候都可以暂停遍历,等下次再接着处理。
从此,协调(Reconciler
)不再是「一口气吃完」,而是可以「咬一小口就停」。
在发现一帧时间已经不够,不能够再继续执行 JS,需要渲染下一帧的时候,这个时候就会打断 JS 的执行,优先渲染下一帧。渲染完成后再接着回来完成上一次没有执行完的 JS 计算。
官方提供了一个 Stack
架构和 Fiber
架构的对比示例:claudiopro.github.io/react-fiber...
下面是 React 源码中创建 Fiber 对象的相关代码:
js
const createFiber = function (tag, pendingProps, key, mode) {
// 创建 fiber 节点的实例对象
return new FiberNode(tag, pendingProps, key, mode);
};
function FiberNode(tag, pendingProps, key, mode) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null; // 映射真实 DOM
// Fiber
// 上下、前后 fiber 通过链表的形式进行关联
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
// 和 hook 相关
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
// ...
}
🚀(2)解决 I/O 瓶颈
Fiber 还引入了一个全新的 Scheduler(调度器)
它的作用是:
- 为不同的更新赋予不同优先级。
- 保证比如用户输入、点击这种高优先级任务能插队执行。
- 如果正在处理一个低优先级的任务,遇到高优先级任务,会先暂停当前任务,去执行更重要的。
React v16之前:
Reconciler(协调器)
:vdom 的实现,根据自变量的变化计算出 UI 的变化Renderer(渲染器)
:负责将 UI 的变化渲染到宿主环境
组件 | 角色 | 功能 |
---|---|---|
Reconciler | 协调器 | vdom 的实现,根据自变量的变化计算出 UI 的变化 |
Renderer | 渲染器 | 将UI的变化渲染到宿主环境 |
从 React v16 开始,多了一个组件:
Scheduler(调度器)
:调度任务的优先级,高优先级的任务会优先进入到 Reconciler
组件 | 角色 | 功能 |
---|---|---|
Scheduler | 调度器 | 调度任务的优先级,支持分级任务,可中断,,高优先级的任务会优先进入到 Reconciler |
Reconciler | 协调器 | vdom 的实现,根据自变量的变化计算出 UI 的变化 |
Renderer | 渲染器 | 将UI的变化渲染到宿主环境 |
在此之后,React 的整体架构演进为:
rust
Scheduler -> Reconciler -> Renderer
新架构中,Reconciler
的更新流程也从之前的递归变成了"可中断的循环过程"。
js
function workLoopConcurrent{
// 如果还有任务,并且时间切片还有剩余的时间
while(workInProgress !== null && !shouldYield()){
performUnitOfWork(workInProgress);
}
}
function shouldYield(){
// 当前时间是否大于过期时间
// 其中 deadline = getCurrentTime() + yieldInterval
// yieldInterval 为调度器预设的时间间隔,默认为 5ms
return getCurrentTime() >= deadline;
}
每次循环都会调用 shouldYield
判断当前的时间切片是否有足够的剩余时间,如果没有足够的剩余时间,就暂停 reconciler
的执行,将主线程还给渲染流水线,进行下一帧的渲染操作,渲染工作完成后,再等待下一个宏任务进行后续代码的执行。这样就避免了长时间占用 JS 线程,带来丝滑的动画与交互体验。
🎯 真题解答
面试官:你了解过 React 的架构吗?Fiber 相比之前的架构优势是什么?
可以这样回答:
React v15及其之前采用Stack架构:
- Reconciler(协调器):vdom 的实现,负责根据自变量变化计算出 UI 变化
- Renderer(渲染器):负责将 UI 变化渲染到宿主环境中
这种架构,在 Reconciler 中,mount 的组件会调用 mountComponent,update 的组件会调用 updateComponent,这两个方法都会递归更新子组件,更新流程一旦开始,中途无法中断。
但是随着应用规模的逐渐增大,之前的架构模式无法再满足"快速响应"这一需求,主要受限于如下两个方面:
- CPU 瓶颈:由于 vdom 在进行差异比较时,采用的是递归的方式,JS 计算会消耗大量的时间,从而导致动画、还有一些需要实时更新的内容产生视觉上的卡顿。
- I/O 瓶颈:由于各种基于"自变量"变化而产生的更新任务没有优先级的概念,因此在某些更新任务(例如文本框的输入)有稍微的延迟,对于用户来讲也是非常敏感的,会让用户产生卡顿的感觉。
新的架构称之为 Fiber 架构:
- Scheduler(调度器):调度任务的优先级,高优先级任务会优先进入到 Reconciler
- Reconciler(协调器):vdom 的实现,负责根据自变量变化计算出 UI 变化
- Renderer(渲染器):负责将 UI 变化渲染到宿主环境中
首先引入了 Fiber 的概念,通过一个对象来描述一个 DOM 节点,但是和之前方案不同的地方在于,每个 Fiber 对象之间通过链表的方式来进行串联。通过 child 来指向子元素,通过 sibling 指向兄弟元素,通过 return 来指向父元素。
在新架构中,Reconciler 中的更新流程从递归变为了"可中断的循环过程"。每次循环都会调用 shouldYield 判断当前的 TimeSlice 是否有剩余时间,没有剩余时间则暂停更新流程,将主线程还给渲染流水线,等待下一个宏任务再继续执行。这样就解决了 CPU 的瓶颈问题。
另外在新架构中还引入了 Scheduler 调度器,用来调度任务的优先级,从而解决了 I/O 的瓶颈问题。