← 回總覽

【第 3667 期】React Fiber 原理解析:从递归渲染到可中断调度

📅 2026-03-11 09:01 前端早读课 软件编程 10 分鐘 11674 字 評分: 78
React Fiber 架构 前端性能优化 JavaScript 调用栈 时间切片
📌 一句话摘要 本文深入解析了 React Fiber 架构的演进动力,阐述了其如何通过自定义链表结构替代原生递归调用栈,实现可中断的异步渲染与时间切片机制。 📝 详细摘要 文章首先探讨了 JavaScript 单线程环境下同步递归渲染(React 15 架构)导致的 UI 阻塞问题,指出原生调用栈一旦开始便无法中途暂停。随后详细介绍了 React Fiber 的解决方案:通过引入包含 child、sibling、return 指针的链表数据结构,React 实现了对渲染过程的精细化控制。这种架构允许 React 将渲染任务拆分为微小的“工作单元”,利用时间切片(Time Slicing)

Title: 【第 3667 期】React Fiber 原理解析:从递归渲染到可中断调度 | BestBlogs.dev

URL Source: https://www.bestblogs.dev/article/ef34ebc0

Published Time: 2026-03-11 01:01:00

Markdown Content: Sanku 2026-03-11 09:01 福建

!Image 1

前言

React Fiber 是 React 架构中的一次关键升级,它解决了早期递归渲染无法中断、任务无法区分优先级的问题。通过将渲染过程拆分为可调度的小任务,并引入 Fiber 数据结构和调度机制,React 可以在渲染过程中暂停、恢复并优先处理用户交互,从而提升界面响应速度。本文将从 JavaScript 调用栈的限制出发,解释 React 为什么需要 Fiber,以及它是如何实现可中断渲染与时间切片的。

今日前端早读课文章由@Sanku分享,@飘飘编译。

译文从这开始~~

> 理解 React Fiber 为什么存在,以及它如何让 React 能够暂停任务、设置优先级

不久前,我在刷 Twitter 时看到了一条帖子。

> @infinterenders: > > react fiber 其实就是那种“很多人以为自己懂了”的东西——直到真正去看底层实现的时候,突然就会觉得这像是某种生活在 JavaScript 里的外星技术。大家经常重复那些很表面的说法,比如“fiber 是新的 diff 算法”。

这是一条非常不错的帖子。它总结了很多关于 React 的内容。在帖子里他提到了一段话,让我开始认真思考,也激起了我进一步了解 React 的兴趣。

> React 终于摆脱了 JavaScript 调用栈的束缚。它不再采用递归渲染,而是通过逐个遍历 Fiber 来实现迭代。这突然意味着 React 可以暂停。就像在渲染中途直接停止,放下一切,让浏览器重新绘制,处理输入,放松一下,喘口气,然后从巨大的 UI 树中中断的地方继续。再也不用担心“哎呀,我深陷递归无法自拔”。因为 React 不再使用内置的调用栈,而是自行实现。

“React 终于摆脱了 JS 调用栈的限制”——嗯?

这点很有意思,因为问题在于:JavaScript 的调用栈到底有什么问题,以至于 React 必须摆脱它? 【第3659期】JavaScript 显式资源管理来了:用 using 告别手写 try/finally

要理解解决方案,我们必须先理解问题:到底是什么限制了 React 的性能?React 团队又提出了怎样的解决思路?

#### 单线程的故事

我们都知道,JavaScript 是一种单线程编程语言。在每个执行上下文中,只有一个线程和一个调用栈,用于逐行处理代码。它从上到下开始处理。 function doSomethingHeavy() {    for (let i = 0; i < 1_000_000_000; i++) {} // 阻塞线程    console.log("Finished heavy task");}doSomethingHeavy();do_important_task(); // 重要任务 从这个例子可以看到,一旦开始执行 doSomethingHeavy 这个任务,它就会一直运行直到结束。假设下面还有一个更重要的任务,那么这个重要任务只有在 doSomethingHeavy 的循环执行完之后才会开始。

这就是一个典型的同步程序。JavaScript 当然也支持异步编程,你很可能使用过 async/await。这些机制是通过 事件循环(event loop) 来实现的。 【第2344期】Javascript是如何工作的:事件循环及异步编程的出现和 5 种更好的 async/await 编程方式

你可能会说,如果我们希望 do_important_task 先执行,只需要把doSomethingHeavy()do_important_task()的顺序交换即可。确实,在这个例子里我们知道哪个任务更重要,因为函数是我们自己写的,我们可以决定执行顺序。

但如果情况是 不确定的(nondeterministic) 呢?

假设程序正在执行一个非常耗时的任务时,突然出现了一个更加紧急的任务。问题是:你无法在程序执行到一半时强行中断它去处理新的任务。你也无法突然清空当前调用栈,然后立刻去处理别的事情。

来看一个示例:

App.js import { useState } from "react";export default function BlockingDemo() {const [count, setCount] = useState(0);const [isBlocked, setIsBlocked] = useState(false);const blockThread = () => {    setIsBlocked(true);    setTimeout(() => {        const start = Date.now();        let dummy = 0;        // 阻塞线程 3 秒        while (Date.now() - start < 3000) {          dummy += Math.random();        }        setIsBlocked(false);    }, 0);};return (  <div className="container">    <div className="count">{count}</div>    <div className="buttons">      <button onClick={blockThread}>        {isBlocked ? "Processing.." : "Start Work"}      </button>      <button onClick={() => setCount(count + 1)}>        Click +1      </button>    </div>  </div>);} !Image 2

可以试一下这个例子:先点击 Start Work,然后在这 3 秒期间点击Click +1。由于 Start Work 会让线程阻塞 3 秒,即使你点击了Click +1,计数也不会立刻增加,而是必须等 3 秒结束后才会更新。

这种体验会让界面显得非常卡顿,用户体验也会很差。

#### React 15

在 React 15 中,reconciler(协调器)是通过递归函数调用来遍历组件树的。一旦渲染过程开始,就无法中途停止。 【第3656期】深入理解 React 的 useEffectEvent:彻底告别闭包陷阱 function updateComponent(component) {  const children = component.render();  children.forEach(child => {    updateComponent(child); // ← 递归调用  });} 每调用一次updateComponent,都会在 JavaScript 的调用栈(call stack)上压入一个新的栈帧。如果组件树有 1000 个组件,那么调用栈里就会有 1000 层嵌套的函数调用。

!Image 3

假设页面上有一个输入框,用户在输入框里输入的内容会实时显示在页面上。用户输入第一个字符s时,React 会开始渲染流程,不断调用updateComponent

由于是递归调用,调用栈会不断堆积函数调用。但如果在渲染进行到一半时,用户又输入了一个字符a,问题就出现了:React 无法停下来。它不能说:“稍等,用户又输入了新的内容,我重新开始渲染。”

JavaScript 本身没有机制可以暂停调用栈、保存当前状态、然后稍后再恢复执行。因此 React 必须先完成对s的整个处理流程,之后才会意识到用户又输入了a

每一次按键都会触发一次完整的 reconciliation(协调过程)。而在每次执行时,React 都被困在递归调用中,这时新的输入事件只能不断排队等待。

这就是为什么早期的 React 应用在某些情况下会显得卡顿(laggy)。

#### 优先级问题

!Image 4

还有第二个问题:React 过去会把所有更新都当作同等重要的任务处理。

例如:

* 按钮点击

* 后台数据请求

* 动画更新

* 日志记录

这些更新在 React 看来优先级是一样的。

但实际上并不是这样。有些更新是非常紧急的(比如用户输入、点击),而有些更新是可以稍后再处理的(例如统计分析、数据预取)。然而 React 以前无法表达这种优先级差异,因为一旦 reconciliation 开始,就必须一直执行到结束。所有任务互相阻塞,优先级完全一样。 【第3654期】不再重置!在 React / Next.js 中实现跨页面“持续进化”的动画效果

举个例子:假设你从服务器获取了一份数据,比如 500 个商品列表。数据返回后,React 开始把这 500 个商品渲染到页面上。当渲染到一半(比如已经渲染了 250 个商品)时,用户在搜索框里输入了一个字母。

这时 React 应该怎么做?

正确的行为应该是:先停止渲染商品列表,优先处理用户输入,立刻更新输入框。

因为用户最关心的是:输入的内容是否立即出现在屏幕上。

商品列表的更新其实可以稍微延迟一点。比如搜索结果晚 100ms 出现,用户几乎不会察觉。

但如果用户输入的字符 100ms 后才显示出来,界面就会显得很卡,甚至像坏了一样。

#### 我们需要什么样的解决方案

!Image 5

在寻找解决方案之前,我们需要明确问题是什么。

使用递归 reconciliation 的模式存在两个主要问题:

* 1、渲染过程无法在中途暂停(因为调用栈不可中断)

* 2、所有更新都被当作同样优先级的任务

因此理想的解决方案应该具备以下能力:

* 可以在渲染过程中暂停

* 当出现更高优先级任务时,先处理高优先级任务

* 然后从刚才暂停的位置继续执行

为什么必须在调用栈层面暂停?

!Image 6

原因在于:浏览器的事件机制就是这样工作的。

只有当调用栈为空时,浏览器才会把宏任务队列(macro task queue)中的事件(例如点击、键盘输入)推入调用栈执行。

换句话说,浏览器就像是在说:“先把你手里的工作做完,然后我再把新的任务交给你。”

如果 React 的递归函数执行时间太长,调用栈一直不清空,那么浏览器就无法把新的点击或输入事件放进调用栈。

这就是界面卡顿的根本原因。

#### 解决方案

!Image 7

React 团队最终意识到,问题不仅仅在于 reconciliation 算法本身,更在于这个算法的执行方式。

简单的性能优化无法解决问题,因为 JavaScript 中的递归本质上是不可中断的。

与其试图让 JavaScript 的调用栈做一些它本来就不擅长的事情,React 团队决定彻底放弃递归模型。

他们不再让 JavaScript 引擎直接驱动整个 reconciliation 过程,而是在它之上构建了一层新的抽象。

这个抽象就是 Fiber。 【第3262期】React的Fiber架构原理

Fiber 并不能控制调用栈——事实上,没有任何东西可以控制它,因为调用栈是 JavaScript 语言本身的一部分。

Fiber 能控制的是:渲染工作的组织方式和执行方式。

以前的方式是:一次性递归完成整个渲染过程。

现在 React 会把整个 reconciliation 过程拆分成很多小的工作单元(units of work)。

执行流程变成:

* 1、React 处理一个小任务

* 2、把控制权交还给浏览器

* 3、调用栈清空

* 4、浏览器处理用户事件(点击、输入、重绘等)

* 5、React 再回来继续处理下一个任务

整个过程会被分成很多很短的时间片(time slices)。

大致流程是:

* React 工作大约~5ms

* 然后主动让出控制权

* 浏览器有机会处理其它任务

* 接着 React 再继续之前的工作

旧版的 Scheduler(大约 React 18 之前的一些实现)在某些路径上使用了固定约 5ms 的时间片策略。而现代 React 的 scheduler 包是基于帧(frame)并且动态调整的。

它会尝试在下一帧截止时间之前完成任务:

* 60fps:约16.7ms

* 120fps:约8.3ms

只有在检测到更高优先级事件或认为安全时,React 才会提前让出控制权。

#### Fiber 作为一种数据结构

React 放弃了递归模型。但如果不再使用调用栈来控制流程,就必须有别的方式来记录当前在组件树中的执行位置。

React 需要一种自己可控的数据结构:它可以在这个结构中遍历、在中途暂停,并且稍后再继续执行。这个结构还必须足够灵活。

这就是 Fiber。

实际上,Fiber 在不同语境下既可以指一种数据结构,也可以指一套算法。

当你编写 JSX 组件时,React 并不会直接把它们渲染到页面上,而是先在内存中构建一棵完整的 UI 树表示。 const fiber ={type:'div',// 这个节点是什么类型child: h1Fiber,// 指向第一个子节点sibling: buttonFiber,// 指向下一个兄弟节点  return: AppFiber,// 指向父节点// ... 还有 React 需要的很多其他信息}; 每一个 fiber 都代表 UI 的一个节点:可能是一个组件、一个 div、一个 button 等等。

注意 fiber 对象里的三个属性:childsiblingreturn

这些其实都是指针,用于把不同的 fiber 连接起来,形成类似链表结构的树形关系。

关键点在于:React 完全掌控这个数据结构。它只是内存中的普通对象,因此 React 可以:

* 按任何方式遍历它

* 在任何位置暂停

* 在任何位置继续执行

这就是 Fiber 的核心意义。

#### Fiber 的遍历方式

!Image 8

##### 步骤 1:移动到子节点

当 React 完成某个 fiber 的工作后,首先会检查这个 fiber 是否有 child。

如果有,那么这个 child fiber 就会成为下一个需要处理的工作单元。

例如在示例中,当 React 完成 form 这个 fiber 的处理后,就会移动到它的子节点。

##### 步骤 2:移动到兄弟节点

如果当前 fiber 没有子节点,React 就会查看是否存在 sibling。

在示例中,input 这个 fiber 没有子节点,于是 React 会移动到它的 sibling。

同样的逻辑会继续发生:遍历 label,然后向下到 span,再到 a。

##### 步骤 3:向上寻找新的工作

如果一个 fiber 既没有子节点也没有兄弟节点,React 就会通过return指针向上回到父节点。

此时 React 会寻找最近一个还存在未访问 sibling 的父节点。

在示例中,当 React 走到某个既没有 child 也没有 sibling 的节点时,它就会回到 form 这个 fiber。

如果父节点也没有 sibling,React 就会继续向上移动,直到找到有 sibling 的节点,或者到达根节点。

##### 步骤 4:到达根节点

遍历会一直持续,直到 React 回到 root fiber,并且没有任何 fiber 需要继续处理。

此时,render 阶段就完成了。

#### 时间切片(Time Slicing)是如何工作的

!Image 9

假设你在输入框里输入内容,这会触发 500 条搜索结果的重新渲染。

过程大致是这样的:浏览器会给 React 大约 5 毫秒的执行时间。时间非常短。

React 开始遍历 fiber 树:

* 先处理第一个 fiber,比如输入框组件

* 更新它

* 然后通过child指针移动到下一个 fiber

* 继续处理

* 再移动到下一个

就这样不断向前。

当处理了几个 fiber(可能是 10 个或 15 个)后,React 会检查时间:“我已经工作 5ms 了吗?”

如果答案是 是的,React 就会停止执行。

它不会把整棵树一次性处理完,而是:

* 停下来

* 记录当前处理到哪个 fiber

* 退出函数

* 清空调用栈

这时浏览器重新获得控制权。

浏览器可以:

* 处理新的键盘输入

* 处理滚动事件

* 处理点击

* 重新绘制页面

当浏览器准备好后,会再次调用 React:“你可以再执行 5ms 了。”

React 会拿出刚才保存的指针:“好的,我刚刚处理到 fiber#15。”

然后从那里继续往下处理。

整个过程会不断重复:

* React 工作 → 浏览器处理事件

* React 工作 → 浏览器处理事件

两者不断配合,直到 500 个 fiber 全部处理完成。

因为浏览器可以在 5ms 内响应每一次按键,所以你的输入看起来是即时响应的。而 React 的渲染则在后台分批完成,不会阻塞界面。

#### 总结

这基本就是 Fiber 的核心思想。

在网上的一些讨论中,经常有人问:为什么 React 要使用 Fiber 架构?像 Vue、SolidJS 等框架使用的是更偏 响应式(reactive) 的方式。

确实,像 Vue 或 SolidJS 这样的框架采用的是 响应式信号(signals)或更细粒度的依赖追踪,只更新真正发生变化的部分。在某些场景下,这种方式会更快。

但这种方式也有自己的权衡。

React 的设计重点在于:

* 灵活性

* 可预测性

* 向后兼容

在 React 中:组件仍然是 state 的纯函数映射,开发者的心智模型保持简单,而 Fiber 会在幕后负责复杂的调度工作。

归根结底,React 依然坚持它最核心的理念:

!Image 10

函数就是 UI 在某个状态下的表现形式。

关于本文

译者:@飘飘

作者:@Sanku

原文:https://inside-react.vercel.app/blog/understanding-why-react-fiber-exists

这期前端早读课

对你有帮助,帮”赞“一下,

期待下一期,帮”在看” 一下。 阅读原文 跳转微信打开

查看原文 → 發佈: 2026-03-11 09:01:00 收錄: 2026-03-11 12:00:44

🤖 問 AI

針對這篇文章提問,AI 會根據文章內容回答。按 Ctrl+Enter 送出。