Skip to content

React 为什么需要 Fiber

Stack Reconciler 的局限与 React 的演进

从某种程度上来说,Fiber 可以理解为 React 为了解决性能问题而引入的一种新数据结构和新的架构。 但是,它是怎么解决的呢?它没有在原有架构上小修小补,而是大刀阔斧的引入了一种全新的机制,将原有的 Stack Reconiler 重写为 Fiber 架构下的 render:具备可中断、可恢复、可排序的纯 JS 计算过程。

Stack Reconciler 被取代,并不是因为“慢”

React 从 Stack Reconciler 走向 Fiber,并不是因为旧版本完全不能用,也不是因为递归本身的有什么问题,而是因为 React 要面对的场景越来越复杂了。

在应用规模较小、交互相对简单时,一次更新同步算完、同步提交,通常没有太大问题。但随着前端应用越来越大、越来越重,React 需要解决的已经不只是“尽快完成一次更新”,而是下面这些更棘手的问题:

  • 页面中同时存在多类更新,它们的重要程度并不相同:这一点可以参考浏览器的具体实现(挖个坑,后面填)。
  • 用户对于不同任务的延迟容忍度不一样:所以需要有显式的优先级。
  • 浏览器中主线程只能同时处理 JS 任务或者渲染任务,过长的 JS 计算会阻塞正常的渲染

所以这里真正的矛盾并不是“慢不慢”,而是 React 希望能够在浏览器调度的基础上,实现一套属于自己的调度机制,实现更高层面的可控

因此我们可以得到一个结论:Fiber 架构需要重塑整个运行时

Stack Reconciler 的工作方式

要理解 Fiber 为什么会出现,首先得了解 Stack Reconciler 是怎么工作的。

在旧架构中,一次更新开始之后,React 会沿着组件树递归向下执行。这个过程非常符合直觉:父组件更新了,就继续处理子组件;子组件需要更新,就再进入更深一层。这样实现起来直接、清晰,而且在早期也足够有效。

  • 更新开始后,会沿组件树递归向下执行。
  • 调用栈由 JavaScript 运行时接管,React 自己无法精细控制中途执行。
  • 一旦开始递归,通常就要一路做完,直到整轮计算结束。

问题并不在于“递归”这件事本身,而在于一旦 React 把整棵树的遍历过程交给 JavaScript 主线程调用,React 就很难再精细地控制这轮工作的推进。JavaScript 调用栈会负责把函数一层层压入、展开、返回,而 React 自己并不能轻易地说:“先停在这里,稍后再继续。”

这意味着,React 在最关键的时刻失去了对于应用的控制权,这才是万恶之源。

Stack Reconciler 的几类局限

所以我们不难理解,Stack Reconciler 会有这样一些缺点

1. 更新过程不可中断

如果一颗组件树很深、很大,那么 render 过程会长时间占用主线程。浏览器此时没法及时处理用户输入、动画和高优先级任务,就容易造成用户感观上的卡顿。

JavaScript 在浏览器主线程上执行时,本来就是单线程推进的。旧的 render 过程一旦开始,React 很难在必要时主动暂停,并把控制权还给浏览器。于是,就出现了卡顿状态

2. 不同更新无法被精细排序

并不是所有更新都同样紧急。

例如:

  • 输入框回显,通常需要立即响应。
  • 页面某个大型列表的重算,可以稍微晚一点。
  • 一些非关键区域的刷新,甚至可以延后到空闲时再做。

旧架构虽然也能做批量更新,但它缺少一种更细粒度的工作调度模型。React 很难把一轮更新拆成很多可管理的片段,再根据当前场景决定哪些先做、哪些后做、哪些应该让位于更重要的任务。

这并不只是“体验优化”层面的小修小补,而是 React 是否能表达“更新之间并不平等”这个事实。只要所有更新都只能被粗暴地打包为一整轮同步工作,那么优先级就很难真正落到执行层面。当然,熟悉操作系统的小伙伴泛起了熟悉的感觉,哎,怎么感觉有点像多优先级任务队列呢?哎,你的直觉没错,我们后面慢慢讲。

3. 缺少恢复与重试能力

如果一轮计算做了一半,环境变了,或者有更高优先级的任务进来,React 理想上应该能:

  • 暂停当前工作
  • 先处理更紧急的任务
  • 之后再从之前的进度继续

但在调用栈递归模型下,这件事并不容易做到。因为大量中间状态都存在运行中的函数栈里,而不是保存在 React 可以随时拿出来操作的数据结构中。React 如果不能接管这些中间状态,就很难真正做到“暂停之后继续”,或者“中途打断后重新安排”。

这也是为什么 Fiber 后来会被称作 React 的“工作单元”基础。因为只有当工作本身被显式表示出来,暂停、恢复、重做这些动作才真正有落点。

4. render 阶段难以真正成为“可调度的纯计算”

React 后来的一个重要思想是:render 可以中断,commit 不能中断。

这句话看上去像是在描述两个阶段的性质,实际上它背后有一个前提:如果 render 过程本身不可拆分,那么“把计算和提交分离”这件事就很难真正发挥价值。只有当 render 被拆成很多可以单独推进的工作单元时,React 才可能在计算阶段做调度、让步、恢复和重试。

从这个角度看,Fiber 真正改变的不是“React 能不能分阶段”,而是 render 这个纯计算过程的完全可控。

React 需要的新能力

综上不难看出,Fiber 其实是 React 为了解决当时遇到的技术债而被倒逼出的一种方案,哎,假如你要问我,为什么 React 当时用了这种更新模型,而不是更细粒度的响应式,那我也先挖个坑,后面慢慢聊。

如果把前面的问题翻译成更明确的设计目标,React 至少需要下面这些能力:

  • 可中断:长任务执行到一半时,可以先暂停。
  • 可恢复:暂停之后,后续可以继续做,不必完全从头来。
  • 可重试:如果中途出现更高优先级任务,旧工作可以被放下并重新安排。
  • 可排序:不同更新有不同优先级,React 需要决定谁先做。
  • render / commit 分离:计算阶段尽量可调度,提交阶段保持短小且原子。

所以,在这些目标的约束下,诞生了 Fiber 架构。OK,那么接下来,我们来探讨 Fiber到底是怎么实现的这些能力

核心抽象

那么,代入 React 维护者的角色,怎么通过 Fiber 来实现我们所需要的能力呢? 下面一一展开

工作单元的概念

理解 Fiber,最重要的一步是先把它看成“工作单元 (work unit)”。

在旧的 React 架构里,React 的很多工作是完全依附于 JavaScript 调用过程的,调用一旦开始,遍历就随着调用栈一层层推进。而在 Fiber 架构里,React 把这部分工作显式地表示成一个个 Fiber 节点。每个 Fiber 都不只是树里的一个点,更代表一份可以被处理、被暂停、被恢复的工作。

所以,React 通过 Fiber 架构,将原本调用栈中的许多隐式调用上下文放到了自己可以操作的数据结构中。那什么叫做隐式上下文呢,其实可以参考一些 LeetCode 题目:LeetCode 112.路径总和,从递归写法改造成迭代写法(类似的题目都可以),所额外增加的信息就是递归调用时的隐式上下文。当然,React 的数据结构要比二叉树负责的多,具体的实现细节我们可以在下一节再展开。

也就是说,React 通过 Fiber 架构,把原本完全交给 JavaScript 的调用栈拿到了自己的手里,从递归改造成了迭代,从而通过 Scheduler 进行更细粒度的调度。

指针

刚刚提到了工作单元这个概念,那具体是怎么通过显式的数据结构设计实现的呢?答案是指针,最常见的三个指针是:

  • child
  • sibling
  • return

它们分别对应当前节点的第一个子节点、下一个兄弟节点,以及父节点。借助这组指针关系,React 可以自主决策遍历时下一步去哪里:

  • 向下进入子节点
  • 向右处理兄弟节点
  • 向上回到父节点

所以通过这三个指针,把曾经完全交给主线程的遍历过程,变成了 React 自主维护的过程,于是,它就有了可以选择在某一个节点上中断与接管的控制权。

原子化的遍历过程

当 Fiber 被定义为工作单元后,render 过程也就不再是“一整段递归调用”,而是可以被拆成很多离散的小步骤。

旧架构更像是一口气把整段工作跑完;而 Fiber 架构允许 React 每次只处理一部分工作。处理完之后,React 可以继续往下做,也可以先停下来,把主线程让给更紧急的事情,等条件合适时再回来接着推进。

这里说的“原子化”,不是指 commit 阶段那种不可分割的原子提交,而是指 render 过程被拆成更细粒度、可以调度的离散工作片段。正是这种工作粒度的变化,才让 React 有机会在 render 阶段引入更加复杂的调度策略。

综上可知,Fiber 架构在 Stack Reconciler 的基础上,真正改变的是一颗 React 应用树的遍历过程,从递归到迭代。

状态持有

此外,React 的另外一个概念也是依赖于 Fiber 实现:函数式组件。它所希望的组件是一个纯函数,然而绝大部分的组件一定具有状态和改变状态的副作用。那么,状态应该在哪里呢?

答案是显然的,React 组件的状态为Fiber所持有。当然,具体怎么持有状态,又怎么进行着状态变更,我们留到下一章节进行拆解。

所以,Fiber 不能简单地理解成是为 React 表达一颗树,它同样承载了 React 的运行时数据状态。

Fiber 如何工作

理解完抽象之后,还需要回到一个更具体的问题:一次更新在 Fiber 架构下,究竟是怎样被推进的?

如果先不展开源码细节,可以先抓住一条足够清晰的主线。

1. 更新产生

某个状态变化、父组件重渲染、context 变化...,都会触发一次新的更新请求。

但这时发生的还不是“立刻去改 DOM”。更新首先会进入 React 的内部工作流,成为一项等待被处理的任务。

我们在代码里调用setState或触发一次重新渲染,看起来像是在要求 React “马上更新界面”,但在内部,它更像是在提交一项新的工作。这也是为什么有的地方会说setState是“异步的”:并不是因为它返回了Promise或需要async/await。更精准地讲setState调用会把更新登记/入队(通常是同步发生的),但它不会立刻进入commit去修改真实界面;是否以及何时推进到render/commit由 React 调度决定,所以开发者观察到的“异步”,更像是调度带来的结果延迟与体验差异。

2. React 为更新安排优先级

不同更新的重要程度不同,因此 React 不会机械地按先来后到处理所有工作。

有些更新必须尽快响应,比如输入反馈;有些更新则可以稍后推进,比如大块 UI 的重新计算。Fiber 架构本身并不等于优先级系统,但它为这种排序与调度提供了基础。

这时,“更新”不再只是一个笼统事件,而开始变成一份带着时机要求的工作。至于这种紧急程度在 React 内部会如何被进一步表达,我们放到后面专门讨论调度时再展开。

3. render 阶段围绕 Fiber 树推进

接下来,React 会围绕 Fiber 树开始 render 阶段的工作。这个阶段的目标不是直接修改真实界面,而是根据最新输入去计算“下一步界面应该变成什么样”,它发生在 React 内部的数据结构与内存状态里,尽量避免产生可观察的副作用:结果先准备好,等后续 commit 才统一落地到真实界面。

在这个过程中,React 会逐步处理每个 Fiber 对应的工作,判断哪些地方需要继续向下计算,哪些部分可以沿用已有结果,哪些变更则留待后面真正提交。

React 在这一阶段会尽可能的计算出下一阶段所需的动作,减少后续 commit 阶段的计算负担,让前面的 render 更像“可调度的描述计算”(因为 render 本质上仍会执行你的组件代码,所以我们说它是“尽量”,而不是绝对纯)。

4. render 过程可以被打断、恢复或重做

这里就是 Fiber 和旧架构最关键的差异。由于 render 不再被绑定在一整段不可分割的递归调用上,React 可以在合适的时机暂停当前工作;如果更高优先级更新进来,也可以先处理更重要的部分;而那些尚未完成的工作,则可以在后续继续推进,或者在必要时重新计算。

于是 React 可以像管理任务队列那样管理渲染过程。

5. 完成 render 后,再统一进入 commit

当 render 阶段完成之后,React 才会带着这轮计算出来的结果进入 commit。

需要再一次强调的是:render 可中断, commit 不可中断。Fiber 改变的不是“React 最终是否要更新界面”,而是“React 以什么方式完成这次更新”。它让前面的计算阶段变得更可调度,但并没有放弃最终提交时的一致性要求。

这也是 Fiber 架构最容易被误解的地方之一。它不是把 React 变成了一个“任何时刻都能随便停”的系统,而是把“可以灵活处理的部分”和“必须一次完成的部分”更明确地拆开了。

常见误解

Fiber 不是单纯为了让 React 更快

很多地方会把 Fiber 简化成一种“性能优化方案”,但更准确地说,它解决的是调度能力问题。

更灵活的调度本身就是有开销的,因此 Fiber 甚至可能在某些局部场景里引入高昂的额外管理成本。它的意义不在于保证每一次更新都更快,而在于让 React 有能力在复杂场景中更稳妥地推进更新。 就像 React/Vue 框架本身不是为了保证每一段代码都快,而是为了保证总体质量一样。

Fiber 不等于 Scheduler

Fiber 和调度系统关系密切,但不是同一个东西。

  • Fiber 提供工作单元和运行时载体
  • Scheduler 负责决定什么时候执行哪些工作

后面讲 Scheduler 时,我们再探讨这一层。

Fiber 也不等于 diff 算法

Fiber 会影响 React 如何推进协调过程,但它不只是“新的 diff 技术”。但 React Virtual DOM 的逻辑实现,确实是依赖于 Fiber。

Fiber 不意味着所有阶段都可中断

最常见的误解之一是:“既然 Fiber 可以暂停,那 React 更新是不是随时都能打断?”

  • render 可以中断
  • commit 不能中断

因为一旦开始对宿主环境做真实提交,React 就必须保证界面一致性。

预告

这一篇写到这里,我们已经可以先得到一个相对清晰的结论:

  • React 为什么不能继续停留在 Stack Reconciler
  • Fiber 试图解决的核心矛盾是什么
  • Fiber 通过怎样的抽象,让更新过程变得可调度

但到目前为止,Fiber 仍然还是一个偏架构层的描述。下一篇就可以继续往下追问:FiberNode 到底是什么?React 又是如何把这些抽象真正落到一个具体的数据结构上的?

只有当这些问题落到具体结构上时,我们才算真正理解了“Fiber 是一种运行时思路”和“Fiber 是 React 内核里的实际工作对象”。