深入理解 React Fiber 架构(上)

287 阅读28分钟

1. 前置知识 📚

1.1 浏览器帧

  1. 帧 (frame):动画过程中,每一幅静止的画面叫做帧。
  2. 帧率 (frame per second):简称 FPS,也叫做浏览器刷新率,即每秒钟播放的静止画面的数量。
  3. 帧时长 (frame running time):每一幅静止的画面的停留时间。
  4. 丢帧 (dropped frame):当某一帧时长高于平均帧时长。

不知道大家有没有玩儿过手翻动画书,对于手翻动画书来说,每一页都是一幅静止的画面,但随着翻页的速度越来越快,这些静止的画面在人的眼中就会开始动起来。这其实就是逐帧动画,把每一帧画到纸上用翻动纸的方式来展示。我们在计算机屏幕上看到的页面内容和翻页动画书相似,其实都是一帧一帧绘制出来的,或者换句话说,我们在计算机屏幕上看到的一切都是由屏幕上播放的图像组成的,这是什么意思呢?如果把计算机显示器当作一本可以自动翻页的动画书,一页相当于是一帧,当屏幕上的事物发生变化时,相当于它在不断地播放,我们之所以感觉不到这个播放动作,是因为播放速度在眼睛看来是瞬间的。人的眼睛存在视觉残像效应——当一帧画面消失时,它留在大脑中的印象还没完全消退,下一帧画面就紧接着出现,这样的时间间隔极短,以至于我们误以为屏幕上的画面是静止的。我们可以用帧率 (FPS) 去衡量这个播放速度,通常图像必须以大约每秒 30 帧 (FPS) 的速率播放才能使人眼感觉流畅和即时,当然,任何更高的东西都会带来更好的体验。

如今,大多数设备都是以 60 Hz,即 60 帧每秒的速度刷新屏幕,每一帧耗时也就是在 16ms 左右 (1s / 60 = 16.67ms ≈ 16ms)。由于原则上说 1s 内绘制的帧数越多,画面表现就越流畅细腻。刷新率 60 Hz 的话,当每秒绘制的帧数(FPS)达到 60 时,页⾯是流畅的。只有渲染一帧时长必须控制在 16.67ms 才能使 FPS 达到 60,如果渲染一帧超过 16.67ms 这一平均帧时长,可能会导致 FPS 达不到 60,FPS 下降时屏幕上的内容会发生抖动,对用户视觉上来说会出现卡顿现象,即丢帧。当然,这对于静态和文本内容来说并不是一个大问题,但是在显示动画的情况下,平均帧时长这个数字非常关键,所以我们书写代码时⼒求不让⼀帧的⼯作量超过16.67ms。

1.2 浏览器渲浏览器

通过上图可看到,浏览器的渲染进程渲染⼀个完整的帧需要完成如下几个步骤的任务:

  1. 接受输入事件,比如用户的输入或者点击等操作
  2. 从合成线程将输入的数据,传递到主线程的事件处理函数
  3. 执行事件的回调处理输入事件
  4. 在绘制之前执行 requestAnimationFrame
  5. 解析 HTML,生成 DOM Tree
  6. 重新计算样式,生成 SCC Tree
  7. Layout 布局,计算 DOM 树中每个可见元素的几何信息,生成布局树 Layout Tree
  8. 更新图层树,为元素的深度进行排序,生成层叠上下文
  9. Paint 为每个图层生成绘制列表
  10. Composite 合成,这一步只是生成了用于合成的数据,并不是真正的合成过程
  11. 将渲染树、绘制指令列表等用于合成的信息提交到合成线程
  12. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图,并通知 GPU 进程刷新这一帧
  13. 执行 RIC (RequestIdelCallback)

注意 ⚠️:浏览器并不需要执行所有步骤,具体情况取决于哪些步骤是必需的。例如,如果没有新的 HTML 要解析,那么解析 HTML 的步骤就不会触发。

如果这几个步骤中,任意⼀个步骤所占⽤的时间过长,总时间超过 16.67ms 了之后,⽤户可能就会看到卡顿。我们以一个例子说明,假设处理输入事件的 JS 执⾏的时间过长,那么就有可能在⽤户有交互的时候,本来应该是渲染下⼀帧了,但浏览器还在执行前一帧的 JS,那么就会导致⽤户交互不能及时得到反馈,从⽽产⽣卡顿感。

事实上,浏览器渲染一帧的过程主要是在 Main Thread 主线程内完成的。在主线程内,浏览器执行着我们熟知的JavaScript,样式,布局和绘制等任务,该线程最容易导致页面内容抖动卡顿,很大程度上是因为它要做的事情太多了,从步骤 3 到步骤 10 以及 13 步骤均在主线程内完成。

最后一步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.67ms 中做完了前面 6 件事儿且还有剩余时间才会执行 requestIdleCallback 里的注册任务。这里提一下,如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,也会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

1.3 时间切片

时间切片是一项使用得比较广的技术方案,该技术方案旨在把一个运行时间比较长的任务分解成一块一块比较小的任务,分块去执行。根据 W3C 性能小组的介绍,超过 50ms 的任务就会被认为是长任务,用户能感知到渲染卡顿和交互的卡顿,所以我们可以缩短任务的连续执行时间。而时间切片就是帮助我们实现这一目的一种技术手段,它的核心思想是:如果任务不能在 50ms 内执行完,那么为了不阻塞主线程,这个任务应该让出主线程的控制权,使浏览器可以处理其他任务。让出控制权意味着停止执行当前任务,让浏览器去执行其他任务,随后再回来继续执行没有执行完的任务。当然,时间切片也是有缺点的,它会让任务运行的总时间变长,这是因为它每处理完一个小任务后,主线程会空闲出来,并且在下一个小任务开始处理之前有一小段延迟。但是为了避免卡死浏览器,这种取舍是很有必要的。时间切片效果的实现还要依赖于 Chrome 的一个 API -- requestIdleCallback,该方法将在浏览器的空闲时段内调用函数。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

2. React 简介

React 是一个流行的 JavaScript 库,开发人员可以使用它来创建复杂和现代的 UI 用户界面。

React 的设计理念是在于对大型项目的快速响应。其官方团队认为它是用 JavaScript 构建快速响应的大型Web 应用程序的首选方式。React 团队对 React 的介绍:

React is, in our opinion, the premier way to build big, fast Web apps with JavaScript. It has scaled very well for us at Facebook and Instagram.

快速响应意味着速度快和响应自然,这可以说是 React 团队在用户体验方面最为要紧的一个追求。那 React 是如何实现快速响应的呢?关键在于虚拟 DOM 和 React Diff 算法。

在 React 中,真实的 DOM 构造会被模拟成一个虚拟 DOM 树结构,每当数据变化时,都会重新构建整个虚拟 DOM 树,然后通过 React Diff 算法对当前整个虚拟 DOM 树和上一次的虚拟 DOM 树进行对比,计算出 Virtual DOM 中改变的部分,最后仅仅将需要变化的部分进行实际的 DOM 操作,而不是每次组件更新时都重新渲染整个真实 DOM。重新渲染整个 DOM 结构的过程中开销是很大的,浏览器需要重新计算布局、样式和绘制,而 Diff 算法能够使得操作过程中只更新修改的那部分 DOM 结构而不更新整个 DOM,这样能够最小化操作 DOM 结构,减少不必要的 DOM 更新,能够最大程度上减少浏览器重绘与回流的规模,因而能达到提高性能的目的。尽管每一次都需要构造完整的虚拟 DOM 树,但是因为虚拟 DOM 是内存数据,性能是极高的。

使用 React 框架,在保证性能的同时,还可以让开发者不再需要关注某个数据的变化如何更新到一个或多个具体的 DOM 元素,而只需要关注状态转移以及最终 UI 长什么样,当数据发生变化,React 框架会自动根据新的状态重新构建 UI,使开发者从复杂的 UI 操作中解放出来。比如,当一条新的消息发送过来时,在传统开发的思路下,开发过程需要知道哪条数据过来了以及如何将新的 DOM 结点添加到当前 DOM 树上;而基于 React 的开发思路,开发者永远只需要关心数据整体,至于两次数据之间的UI 如何变化,则完全交给框架去做。因为 React 的核心思想就是跟踪组件状态变化并将更新后的状态更新到屏幕上。

React 为了践行构建快速响应的大型 Web 应用程序理念做出了很多努力。比如传统 Diff 算法计算一棵树形结构转换成另一棵树形结构的时间复杂度最优解是 O(n^3),那么如果有 1000 个节点,则一次 Diff 就将进行 10 亿次比较,这显然无法达到高性能的要求。而 React 通过大胆的假设,并基于假设提出相关策略,成功的将 O(n^3) 复杂度的问题转化为 O(n) 复杂度的问题。随着时间的推移和业务复杂度的提升,为了更进一步贯彻“快速响应”的原则,React 的优化仍在继续,比如 React 16 中新引入 Fiber,它是 Facebook 花费两年余时间对 React 做出的一个重大改变与优化,使 React 在性能上又有了质的飞跃。而本篇文章主要就是对 React fiber 架构进行一个整体分析,我们可以下带着以下这些问题去阅读后续内容:

  1. React fiber 是什么?
  2. 为什么会出现 React fiber 架构?
  3. ……

3. React Fiber 是什么 🤔

💡 React Fiber 是⼀个从 React v16 开始引⼊的新协调算法,⽤来实现 Virtual DOM 的增量渲染。

上述对 React Fiber 的解释说明是不是有些生涩难懂?这种感觉仿佛就像是在初学文言文,陌生的术语让人很难理解其表达的意思。在中学语文课文中,文言文的通常都会有相应的注释,用来对文章内容进行解释和说明,从而帮助正确理解难词难句进而深入掌握内容。如果把上述对 React Fiber 的解释说明这句话当成一句文言文,那么需要补充 3 个术语注释才能使其更加的通俗易懂,这 3 个术语注释分别为协调,Virtual DOM 和增量渲染。(⚠️:由于在前文中我们已经对 Virtual DOM 有过简单介绍,因此这里只对协调和增量渲染进行解释。)

协调:React 的核心就是跟踪组件状态变化并将更新后的状态更新到屏幕上。在某一时间节点调用 React 组件的 render 方法,会创建一棵由 React 元素组成的虚拟 DOM 树。在下次 state 或 props 更新时,相同的 render 方法会返回一棵不同的虚拟 DOM 树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前的UI 与最新的树保持同步。为了实现该核心要点,React 采用了一种算法来比较两棵树并获得两者之间的差异,从而帮助 React 确定它需要在屏幕上更改什么,这种算法就被称为协调,也就是我们熟知的 Diff 算法。

增量渲染:用来解决掉帧的问题,将渲染任务分成块并将其分散到多个帧上。渲染任务拆分之后,每次只做一小段,做完一段就把时间控制权交还给主线程,避免长时间占用主线程。

熟悉了这 3 个术语后,是不是稍微有一点豁然开朗的感觉?其实,简单来说, React Fiber 就是对 React 核心算法的一次重新实现,该算法让 React 视图更新过程变得更加流畅顺滑。

4. 老的 React 15 架构

在前面对 React 进行简要介绍的时候有说到,React Fiber 是 Facebook 花费两年余时间对 React 做出的一个重大改变与优化。你可能会问,为什么 React 团队要花费两年时间去做这样的一个改变与优化呢?在讨论这个问题之前,我们得先了解一下 React 16 之前的 React 架构。

4.1 架构分层

React 15 架构分为:reconciler 协调器和 renderer 渲染器。

我们在上一节里介绍了协调的概念,reconciler 协调器的核心工作就是比较新的虚拟 DOM 与旧的虚拟 DOM,找出变化的部分,并决定哪些部分需要更新,即通过 Diff 算法找出最小的差异,生成必要的更新操作。而渲染器则主要是负责将协调器的输出(找出的最小差异)转化为真实的 DOM 操作,并将这些操作应用到浏览器的 DOM 上。

我们知道,在 React 中可以通过 this.setState、this.forceUpdate、ReactDOM.render 等 API 触发更新。每当有更新发生时,Reconciler 会做如下工作:

  1. 调用函数组件或类组件的 render 方法,将返回的 JSX 转化为虚拟 DOM 树;
  2. 将虚拟 DOM 树和上次更新时的虚拟 DOM 树对比,通过对比找出本次更新中变化的虚拟 DOM 节点;
  3. 通知 Renderer 将变化的虚拟 DOM 节点渲染到页面上。

Renderer 接到 Reconciler 的通知,通过调用宿主环境的 API 将变化的 VDOM 重新渲染到当前宿主环境。由于 React 支持跨平台,所以不同平台(宿主环境)有不同的 Renderer 渲染器。我们前端最熟悉的是负责在浏览器环境渲染的 Renderer – ReactDOM,除此之外,还有:

  1. ReactNative 渲染器:APP 原生组件渲染
  2. ReactTest 渲染器:渲染出纯 JS 对象用于测试
  3. ReactArt 渲染器:渲染到 Canvas、Svg 等容器上

……

图摘自 React Conf 2017 Lin Clack 的演讲 — A Cartoon Intro to Fiber

4.2 更新机制

在 React 15 架构中,Reconciler 使用的协调算法是堆栈协调器算法。该算法采用的是树型结构的虚拟 DOM 树,它基于递归的方式遍历虚拟 DOM 树并进行对比更新。递归意味着同步执行,⼀旦这个过程开始就不可中断,直到整棵虚拟 DOM 树计算完成。

然⽽,当组件树⽐较庞⼤时,这种机制的问题就来了,举个简单的例子:⼀颗拥有 300 个组件的组件树需要全部更新,假设⼀个组件更新只需要耗时1ms,整棵树更新⼀次就需要耗时 300ms。在这 300ms 期间,浏览器的主线程⼀直在专⼼致志地忙着更新这颗组件树(这时函数的调⽤栈会⾮常长),对于页⾯上的任何操作都是不闻不问的。在这期间,假如⽤户在⼀个输⼊框敲了⼏个字,页⾯上是不会有任何反应的,因为渲染按键输⼊结果也需要主线程来做,然⽽此时主线程正忙着更新组件树呢。等到 300ms 结束了,浏览器主线程有空了,才把刚刚敲的那⼏个字渲染到 input 输⼊框内。

由于 JavaScript 的单线程⼯作特点,业内⼀直有个这样的原则:任何动作都不要长时间霸占主线程,如果迟迟不归还主线程,那么在这期间程序就没法对其他输⼊作出响应。输⼊了却没有响应,或者说响应来的很慢,也就是我们常常说的“卡顿”。显然,React 的同步更新机制在组件树庞⼤时就违反了这⼀原则。

你可能会问:为什么 Javascript 执行占据主线程时间较长时会导致页面响应度变差,使得动画、手势交互等事件产生卡顿?这是因为浏览器中 JS 执行 和 UI 渲染是互斥的,执行 JS 时无法进行 UI 渲染,长时间执行 JS 会导致 UI 渲染长时间挂起,页面就会卡顿甚至直接卡死。在 1.2 小节中对浏览器的渲染过程有简单的介绍,回顾 1.2 小节你会发现 JavaScript 在浏览器的主线程上运行,它与样式计算、布局以及绘制等这些 UI 渲染步骤同步的。如果JavaScript 运行时间过长,就会阻塞后续的其他工作,可能导致掉帧。

为什么被称为堆栈协调器呢?这个名字其实来源于栈数据结构。该数据结构遵循先进后出,后进先出的规则。打个比方,它就像一个“封底的集装箱”,货物只能从上端加入或取出,对应的术语叫“压入”和“弹出”。递归常用到调用栈,递归的实现通过对调用栈的“压入,弹出”完成,即先往调用栈中压入数据从而实现“递”,然后再弹出数据实现“归”,在归的过程中每一步都会返回一个值,直到到达线性条件才终止。堆栈协调器算法构建虚拟 DOM 树的过程中,数据就是保存在递归调用栈中的。

在 React Conf 2017 大会上,Lin Clark 在 A Cartoon Intro to Fiber 演讲中用了一张卡通图片来描述堆栈协调器:

这张卡通图代表着什么意思呢?它其实表示的是堆栈协调器算法的递归这个过程。

图片里面的火柴人好似一个潜水员,当它一头扎进水里,就要往最底层一直游,直到找到最底层的组件,而后他再上岸。 在这期间,岸上发生的任何事,都不能对他进行干扰,若是有更重要的事情须要他去作,如用户操做反馈,也必须得等他上岸。

我们用组件的渲染顺序来对其加以解释说明。假如有 A、B、C、D 组件,层级结构为:

我们知道组件的生命周期在挂载阶段为:

constructor()

componentWillMount()

render()

componentDidMount()

更新阶段为:

componentWillReceiveProps()

shouldComponentUpdate()

componentWillUpdate()

render()

componentDidUpdate

那么在挂载阶段, A、B、C、D 的生命周期渲染顺序是如何的呢?

从上图可以看出,整个过程以 render 函数为分界线,从顶层组件开始一直往下,直至最底层子组件,然后再往上,组件 update 阶段同理。

除此之外,Reconciler 和 Renderer 还是同步交替执行的。每当 Reconciler 计算出一个差异时,都会立即通知 Renderer 渲染这个变化,并在 Renderer 渲染完之后再继续进行下一个节点的计算,直到整棵虚拟 DOM 树计算更新完成。

5. 新的 React Fiber 架构

在解释 React Fiber 是什么的时候有说到, React Fiber 让 React 的视图更新过程变得更加流畅顺滑。从这里其实就可以猜测在 Fiber 之前的架构下,React 的视图更新是存在着性能问题的,事实证明这个猜想是正确的。在 React fiber 架构之前,React 在协调阶段使用递归方式进行节点遍历,这种⽅式有⼀个特点:⼀旦任务开始进⾏,就⽆法中断,那么 JS 将⼀直占⽤主线程,⼀直要等到整个虚拟 DOM 树计算完成之后,才能把执⾏权交给渲染引擎,那么这就会导致⼀些⽤户交互、动画等高优先级的任务⽆法⽴即得到处理,就会有卡顿,⾮常的影响⽤户体验。

不难发现该性能问题的根源在于:

  1. 递归遍历节点树,无法中断遍历;
  2. 遍历节点树会一直占用主线程,阻塞了浏览器的其他线程。

React 官方团队希望能够彻底解决主线程长时间占用问题,于是决定将递归的无法中断的更新重构为异步的可中断更新,其设计思路为:把更新过程 🧩 碎片化,即把整个大的渲染/更新任务拆分成若干小的任务单元,这样就能中断长任务,去做一些更高优先级的任务。设计思路是有了,但是 React15 的架构支持异步可中断更新么?答案是否定的,因为在老的架构中 Reconciler 和 Renderer 是交替工作的,当有一处变化的时候,Reconciler 比对完变化就立马给到 Renderer 去渲染,然后再继续寻找变化,如果中途断掉的话,可能会导致用户看到的页面是更新不完全的页面。因此,React 不得不从整个架构上进行改造。于是,全新的 Fiber 架构应运而生。

5.1 架构分层

在 React fiber 中,为了异步的可中断更新,两层架构变成了三层架构:

  1. scheduler 调度器:排序优先级,让优先级高的任务先进入 Reconciler
  2. reconciler 协调器:找出哪些节点发生了改变,并打上标签进行标记
  3. renderer 渲染器:将 Reconciler 中打好标记的节点渲染到视图上

与 V15 版本不同的是,React Fiber 架构新增了 scheduler 调度器,使得高优先级任务优先进入 reconciler 协调器,并且协调器使用了更先进的 fiber reconciler 协调算法,它是 React Fiber 架构的核心,正是该算法实现了异步可中断更新,从而降低了页面卡顿的概率,提升了页面的交互体验。

5.2 更新机制

在 React Fiber 新架构中,协调器和渲染器不再交替执行。更新的处理工作流变成了这样:

当组件状态变化时,会触发更新请求,这个请求被送到调度器,每个抵达调度器的更新任务都会被赋予一个优先级,更高优先级的更新会优先进入协调器。

协调器接收到任务后,它会根据优先级和当前的时间片,开始增量地处理虚拟 DOM 的对比和更新过程。如果在这个过程中遇到更高优先级的任务,当前的更新任务会被暂时中止,协调器会优先处理高优先级的任务。由于调度器和协调器都是在内存中工作,不会影响视图,因此即使中断用户也不会看到更新不完全的视图。

在协调过程中,协调器会将需要更新的节点打上标记,当某次更新完成了在协调器中的工作时,也就是在所有差异找出之后,协调器再统一通知渲染器,渲染器接收到通知,查看有哪些被打了标记的虚拟 DOM,对这些带标记的虚拟 DOM 执行更新真实 DOM 的操作,包括 DOM 节点的增删改查。当高优先级的更新最终完成了渲染,调度器又会开始新一轮的调度,之前被中断的更新将会被重新推入 Reconciler 层,继续它的渲染之旅,这便是所谓可恢复。

5.2 设计思路详述

前面有对 Fiber 架构的设计思路进行概述:把更新过程 🧩 碎片化,即把整个大的组件渲染 / 更新任务拆分成若干小的任务单元,这样就能中断长任务,去做一些更高优先级的任务。该设计思路更为具体的来说,就是在执行工作单元之前,由浏览器判断是否有空余时间执行,有时间就执行工作单元,每执行完一个小的任务单元,都会看看有没有什么其他紧急任务要做。如果发现有紧急任务,那么会马上停掉当前更新任务,转而让主线程去做紧急任务,等主线程做完紧急任务,再重新做被中断的更新任务;如果没有紧急任务,才继续做接下来的任务单元。当分配的时间切片用完了,也需要将控制权返还让浏览器执行其他任务,比如页面的渲染,等到下一帧执行时判断是否有空余时间,有时间就从终止的地方继续执行工作单元,一直重复到任务结束。

这其实有点像操作系统中优先级抢占调度时间片轮转调度的结合:将任务划分不同优先级级别,允许存在相同优先级,针对不同优先级间采用抢占式调度方式,针对相同优先级间,采用时间片轮转调度方式,从而实现既能够兼容抢占式的实时性,又能够兼容时间片轮转调度的合理分配资源。

图摘自 React Conf 2017 Lin Clack 的演讲 — A Cartoon Intro to Fiber

这个过程如上图所示。如果我们仍然把图中的小火柴人比作一名潜水员,那么这张卡通图代表的意思就是让潜水员会每隔一段时间就上岸,看是否有更重要的事情要作。即中间每一个波谷代表深入某个任务单元的执行过程,每个波峰就是交还控制权的时机。也就是说,在规定的时间间隔结束后,不管这个任务单元有没有执行完成,潜水员都需要暂停任务,上岸去透口气,随便询问有没有更紧急的任务需要插队执行,等到能再次下水的时候再次跳入水中执行任务(如果有更紧急的任务,那么潜水员再次下水所执行的任务就是这个紧急任务;如果没有,就会继续执行上次中断的任务),在潜水员上岸透气休息到下次再入水的这段时间,主线程有机会去执行一些其他任务,比如让渲染线程去执行渲染页面的任务。

综上所述,Fiber 架构的设计思路本质上其实就是将长任务分拆到浏览器每一帧中,每一帧执行一小段任务的操作,即我们在前置知识小节中提到的时间切片。

6. 老架构 🆚 新架构

前面两个小节分别对老的 React 15 架构和新的 React Fiber 架构进行了简单介绍,接下来我们会以表格的形式对两者进行对比,使我们能够更加清晰的、一目了然的看到新老架构之间的区别,并使用一个 Demo 来展示它们之间的性能 PK,体验 Fiber 架构在性能上带来的优化。

6.1 架构对比

版本React 15React Fiber
架构组成Reconciler 协调器、Renderer 渲染器Scheduler 调度器、Reconciler 协调器、Renderer 渲染器
核心算法Stack ReconcilerFiber Reconciler
更新方式Reconciler 与 Renderer 交替进行先由 Scheduler 调度器调度出优先级高的任务,再由 Reconciler 标记更新(Reconciler 调和阶段中途如果发现更高优先级的任务,会中断当前任务,优先执行紧急任务,等待紧急任务执行完毕后再重新执行当前任务),整个组件标记完成后由 Renderer 渲染
存在的问题调和阶段使用递归方式对虚拟 DOM 树进行节点遍历,如果组件层级较深,会一直占用主线程,阻塞浏览器的其他线程。并且由于 Reconciler 与 Renderer 交替进行,如果强行中断遍历,会导致页面更新不完全
特点同步不可中断更新异步可中断更新

6.2 性能对比

新的 Fiber 架构将原有的核心算法 Stack Reconciler 替换为 Fiber Reconciler,提高了复杂应用的可响应性和性能。A Cartoon Intro to Fiber 演讲中还用了一个使用 React 渲染一个不断缩小和放大的谢尔宾斯基三角形的例子来对比展示 Stack 和 Fiber 之间的区别。

PS ⚠️:你可以通过 Attention Required! | Cloudflare 来看一下这两者之间的区别。

这个不断缩小和放大的谢尔宾斯基三角形例子进行了 2 中不同的更新,一种是三角形变窄和变宽的动画更新,另一种则是构成三角形的每个点中包含的数字从 0 到 10 循环更新,而动画和数字这两种更新之间是有主线程争夺的冲突的。该示例非常适合分析分配不同优先级的情况,我们希望能够为不同类型的更新分配不同的优先级。使三角形变宽和变窄的动画更新比数字的更新更重要。因为如果数字更新有些延迟,用户可能甚至不会注意到它,但是如果动画开始丢帧,用户很快就能发现它。

通过对比很明显可以看出,这个例子在 React 15 中掉帧非常严重,而在React Fiber 中就要流畅很多。这是因为新的 Fiber 架构改变了之前 react 同步的组件渲染机制,使原来同步渲染的组件现在可以异步化,可中途中断本次渲染任务,执行更高优先级的任务,用户体验更好。

注意 ⚠️:在一般的应用下是不会有这么明显的差别的,上面的范例只是刻意为之,但当应用非常复杂并且非常吃效能时,上面这种状况就有可能会发生。

总结

我们前面所讨论的是 Fiber 架构的架构分层和宏观视角下的设计思路,但这一切都还只是学习 Fiber 架构的一个开端,其具体的工作流目前来说仍然是一个黑盒,这个黑盒里装着许多谜题的答案,比如 Fiber 架构是如何将任务碎片化拆分成任务单元的?任务的优先级调度是如何实现的?怎么判断是浏览器主线程此时是否空闲?可中断和可恢复又到底是如何实现的等等?接下来我们要做的事情就是去一起去探索这个黑盒,让这个黑盒逐渐变得透明。

下篇文章我们将揭晓这些谜题,深入探讨 React Fiber 的实现原理。通过了解 Fiber 的实现细节,更好地理解 React 组件更新的高效策略。

资料

深入了解现代网络浏览器(第 1 部分)CPU、GPU、内存和多进程架构

深入了解现代网络浏览器(第 2 部分)在导航过程中会发生什么

深入了解现代网络浏览器(第 3 部分)渲染程序进程的内部工作方式

深入了解现代网络浏览器(第 4 部分)输入即将到达合成器

Aerotwist - The Anatomy of a Frame

这可能是最通俗的 React Fiber(时间分片) 打开方式

React 开发者一定要知道的底层机制 — React Fiber Reconciler

React Conf

A Cartoon Intro to Fiber