Introduction to React Fiber
❓ What is React Fiber?
React Fiber is a completely backward-compatible reimplementation of React's core algorithm that is shipped with React >= 16.8. The goal is to increase its suitability for areas like animation, layout, and gestures.
React Fiber is a reimplementation of the React's reconcilization algorithm introduced since React v16.8, which aims to solve known performance issues in the previous implementaion due to its synchronous nature.
With asynchronous design in mind, React Fiber introduced a Schdeuler to render tasks in a more efficient way by scheduling . The fundamental that made it possible is the Fiber Architecture, which allows React to:
- Split render tasks into smaller chunks for scheduling
- Assign priority to different types of updates
- Pause, abort, or reuse work when needed
Based on the above features, React Fiber has significantly improved the performance of React as component tree grows larger when UI gets more complicated over time. Check the Demo to visualize the performance difference of the Fiber architecture and the previous Stack-Based architecture.
Aside from enhancement in performance, responsiveness and UX, React Fiber is also the stepstone to other important features such as: React Hooks Suspense and Concurrent Mode.
🚄 Train of Thoughts
What problems does React Fiber intend to solve ?
Problems before React Fiber introduced include but not limited to:
- Reconciler implementation is stack-based, such that render tasks must be done synchronously and can’t be paused or get back to where it got interrupted. This is bad when events with higher priority(e.g user input) get delayed causing bad user experience.
- Complex render tasks might take longer time to complete and result in a lower frame rate.
- Executing too much work all at once can block main thread due to JavaScripts single-thread nature.
- Some works may be superseded by a more recent update and becomes unnecessary but can't be aborted.
- Fetching data might leave React nothing to render before data is received, leaving React nothing to render.
These CPU-bounded and I/O-bounded issues both result in a poor user experience.
What approaches are introduced to tackle with aforementioned problems?
To solve aforementioned problems, two concepts are introduced in React: Time slicing
to solve CPU issues and Suspense
to solve I/O issues. Refer to Dan Abramov's talk for more in-depth concept. To carry out these concepts, reimplementation of the React reconcilier is needed.
Quoting from Andrew Clarks' note,
We've established that a primary goal of Fiber is to enable React to take advantage of scheduling. Specifically, we need to be able to
Pause work and come back to it later. Assign priority to different types of work. Reuse previously completed work. Abort work if it's no longer needed. In order to do any of this, we first need a way to break work down into units. In one sense, that's what a fiber is. A fiber represents a unit of work.
Reconcilier is reimplemented with a linked-list-based strucutre, enabling Reconciliation process to be paused, aborted or returned after interrupted.
Introduction of Fiber:
- Fiber represents an unit of work for React to process, which can be seen as
React.Element
extended with more properties. - Old reconciler creates an immutable tree of
React.Element
(also known as virtual DOM) to perform diff algorithm by traversing the tree recursively. New reconciler instead creates a tree ofFiber Node
iteratively.
- Fiber represents an unit of work for React to process, which can be seen as
A
Scheduler
is introduced to schedule execution of tasks. Scheduler originally uses `window.requestIdleCallback(rIC)`` API for task scheduling, but the core team found some pitfalls such as:- Browser compatibility issue
- rIC is not triggered in a stable manner if tab switching presents
Thus,
Scheduler
is in fact a more roundedrequestIdleCallback
polyfill, which replaced the rIC with apostmessage
loop with short intervals so that scheduling is not limited to align to frame boundaries. Refer to facebook/react issue #16214.After the
scheduler
is introduced, the steps of reconciliation process has an additional step.Heuristic algorithm is implemented based on a few assumptions, that reduces the time complexity from O(n^3) to O(n).
🕵️ Deep Dive
What is Fiber?
Broadly speaking, React Fiber is a complety rewrite of the reconciler to improve performance, but it is in fact a JavaScript object on a narrow scope which, refering to Andrew Clarks's note, contains information about a component, its input, and its output.
ReactFiber.js (as of 2023/07)
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
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;
if (enableProfilerTimer) {
// Note: The following is done to avoid a v8 performance cliff.
//
// Initializing the fields below to smis and later updating them with
// double values will cause Fibers to end up having separate shapes.
// This behavior/bug has something to do with Object.preventExtension().
// Fortunately this only impacts DEV builds.
// Unfortunately it makes React unusably slow for some applications.
// To work around this, initialize the fields below with doubles.
//
// Learn more about this here:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
if (__DEV__) {
// This isn't directly used but is handy for debugging internals:
this._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}
Crucial implementations are:
Instance
Sectionthis.tag
: Identify the type of the component, exhaust list can be seen here.this.key
: Key field that serves as an identifier to determine whether a fiber can be reusedthis.elementType
: Mostly same as type, but differ in some cases such asmemoized component
.this.type
: Identifies the type of DOM node, e.gdiv
,span
, ... etc..this.stateNode
: Points to the actual DOM node
type
describes the component that it corresponds to. For composite components, the type is the function or class component itself. For host components (div
,span
, etc.), the type is a string. Bothtype
andkey
serve the same purpose as they do for React elements. In fact, when a fiber is created from an element, these two fields are copied over directly.Note that `type` is used in the heuristic algorithm, or the reconciliation, to speed up the whole compare progress. If `current fiber` and `work-in-progress fiber` of a given element has different `type`, reconciliation progress stops here and subtree with that exact element as root will be rerendered.
Fiber
SectionThese fields point to other fibers, describing the recursive and tree-like component structure -- a fiber tree.
this.return
: Points to the fiber to which reconciler should return after processing the current one. It is conceptually the same as the previous stack frame in stack-based reconciler. If a fiber has multiple child fibers, each child fiber's return fiber is the parent.this.child
: Corresponds to the value returned by a component'srender
method. The child fibers form a singly-linked list whose head is the first child of the current fiber.this.sibling
: Accounts for the case where render returns multiple children, meaning components at the same level form a singly-linked list where each fiber points to the immediate next fiber.
These three pointers form a linked-list structure which is the foundation of fiber architecture.
this.pendingProps
&this.memoizedProps
: A fiber's pendingProps are set at the beginning of its execution, and memoizedProps are set at the end. When the incoming pendingProps are equal to memoizedProps, it signals that the fiber's previous output can be reused, preventing unnecessary work.this.alternate
: At any time, a component instance is referred to by at most two fibers,- Current Fiber: Fiber that has been committed and rendered onto the screen.
- Work-in-Progress: A fiber that has not yet been committed.
The alternate of the current fiber is the work-in-progress, and the alternate of the work-in-progress is the current fiber.
A fiber's alternate is created lazily using a function called createFiber. Instead of always creating a new object, createFiber uses a double buffering pooling technique that will attempt to reuse the fiber's alternate if it exists, which allows minimizing memory allocations.
Line 24~30 are utilized to memoize state of the fiber across rerenders.
Lane Model
To enable task scheduling, scheduler was first implemented in a expiration time model
but replaced with the current lane model
in 2020 to allow dynamic priority manipulating. More details to the initiative can be refered in this PR.
How does Reconciler work ?
React developers should be familiar with JSX and how React forms a virtual DOM based on JSX. Now with the fiber
object, React performs an additional step by calling a createFiberFromTypeAndProps function to creates a fiber tree
.
- JSX → React calls
React.CreateElement
→ React.Elements → React Fiber calls createFiberFromTypeAndProps → Fiber Node
Reconciler is responsible for identifing elements that need to be updated. During reconciliation, it can be divided into two phases: render
phase and commit
phase.
To efficiently find out diff the current tree and work-in-progress tree, React implements a heuristic O(n) algorithm based on two assumptions:
- Two elements of different types will produce different trees.
- The developer can hint at which child elements may be stable across different renders with a key prop.
In practice, these assumptions are valid for almost all practical use cases.
Reconciliation details can be checked here.
How does Scheduler work ?
How does React process a fiber tree ?
WIP
child -> self -> sibling
❓ What happens within one frame?
🔗 References
- React Reconcilier: Fiber v.s Stack Version
- Lin Clark - A Cartoon Intro to Fiber - React Conf 2017
- React Fiber Architecture Design Note
- React Basics
- Dan Abramov: Beyond React 16 | JSConf Iceland
- Andrew Clark: What's Next for React — ReactNext 2016
- React 開發者一定要知道的底層架構- React Fiber
- The Comprehensive Guide to React’s Virtual DOM
- Inside Fiber: in-depth overview of the new reconciliation algorithm in React
- Use a Symbol to tag every ReactElement #4832
- React Fiber很难?六个问题助你理解 React Fiber
- Sebastian Markbåge - React Performance End to End (React Fiber) - Keynote Part 3 - React Conf 2017
- Concurrent Rendering in React - Andrew Clark and Brian Vaughn - React Conf 2018