跪拜 Guibai
← Back to the summary

React Fiber's 20 Fields Are the Entire Runtime

Memory Leak Investigation

I previously took over a React project that had been running for two years. Users reported that the page would slow down after being open for a long time, only recovering after a refresh. Chrome DevTools' Memory panel showed that about 15MB of memory could not be reclaimed after each route switch. 15MB doesn't sound like much, but users' office computers keep the page open all day, and it accumulates to several GB.

I opened a Heap Snapshot, ready to catch the memory leak. The result stunned me.

The leaked objects were not closures, not event listeners, not global variables—but FiberNodes. Tens of thousands of FiberNode objects lay in the heap, each about 300 bytes, but they were entangled like vines, with child pointing to one, sibling to another, return to the parent, and alternate to the corresponding node in the other tree. These pointers wove a giant net, and the garbage collector got lost in this net, unable to determine which nodes should be reclaimed and which shouldn't.

I spent three days tracing the origins of these FiberNodes. On the second day, I stared at the FiberNode constructor in ReactFiber.js for a full hour. It was a piece of simple code—no clever algorithms, no complex design patterns, just a bunch of this.xxx = null. But it was precisely these assignment statements that defined the entire React runtime world.

function FiberNode(tag, pendingProps, key, mode) {
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  this.ref = null;
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.memoizedState = null;
  this.updateQueue = null;
  this.dependencies = null;
  this.mode = mode;
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  this.alternate = null;
  // ...
}

I counted—over twenty fields. Not many. But each field shoulders a mission—some describe the component's identity, some link the tree's structure, some record the work's state, some mark side effects. The combination of these twenty-plus fields supports React's entire runtime—component tree, state management, scheduling priority, side effect tracking, double buffering... all are here.

I closed DevTools and took a sip of cold coffee. The root cause of the problem was found—a third-party routing library forgot to disconnect the root node reference of the Fiber tree during unmounting. A three-line fix. But my understanding of the Fiber data structure deepened more in those two days than in the past year.


1. Virtual DOM Is No Longer Enough

Early React (v15 and before) used Virtual DOM to describe the interface. What is Virtual DOM? Essentially, it's a tree of ReactElements—nested object structures, each object having type, props, children.

{
  type: 'div',
  props: {
    className: 'container',
    children: [
      { type: 'h1', props: { children: 'Hello' } },
      { type: 'p', props: { children: 'World' } }
    ]
  }
}

This structure has two fatal problems.

First, it only describes "what it should look like," not "what work needs to be done." Every setState requires React to traverse the entire tree from scratch, compare old and new differences, and decide which nodes to update. The intermediate state generated during traversal—where it has reached, where to go next, what side effects need to be executed—is all stored on JavaScript's call stack. The result is that once traversal begins, it cannot be interrupted, because the information in the call stack is lost after the function returns.

Second, it is immutable. Each update creates a brand new tree. The old tree is discarded, the new tree takes over. This is fine for simple applications, but for scenarios requiring incremental updates (like animations, high-frequency input), the performance cost is huge.

The Fiber data structure was born to solve these two problems. Its core design judgment is: a node should not only describe the UI, but also describe the work. Each Fiber node is a unit of work—it knows what type of component it is, which upstream and downstream nodes it links to, what pending updates it has, what priority it belongs to, and what side effects it needs.

This is a paradigm shift at the data structure level—from a "tree describing UI" to a "linked list describing work."


2. The Four Identities of a Fiber Node

Open packages/react-reconciler/src/ReactInternalTypes.js and look at the complete definition of the Fiber type:

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {
  // ===== First Identity: Component Description =====
  tag: WorkTag,              // Component type tag (Function/Class/Host/Text...)
  key: null | string,        // React key
  elementType: any,          // ReactElement.type (unresolved)
  type: any,                 // Resolved component (function ref/class/string tag)
  stateNode: any,            // Associated real instance (DOM node/component instance)

  // ===== Second Identity: Tree Structure =====
  return: Fiber | null,      // Parent node
  child: Fiber | null,       // First child node
  sibling: Fiber | null,     // Next sibling node
  index: number,             // Position index in parent's children

  // ===== Third Identity: Work State =====
  pendingProps: any,         // New props (pending processing)
  memoizedProps: any,        // Props used in the last render
  memoizedState: any,        // State used in the last render
  updateQueue: mixed,        // State update queue
  dependencies: Dependencies | null,  // Context dependencies
  mode: TypeOfMode,          // Running mode
  lanes: Lanes,              // Work priority of this node
  childLanes: Lanes,         // Work priority of the subtree

  // ===== Fourth Identity: Side Effect Tracking =====
  flags: Flags,              // Side effects of this node
  subtreeFlags: Flags,       // Side effects of the subtree
  deletions: Array<Fiber> | null,  // Child nodes to be deleted
  alternate: Fiber | null,   // Corresponding node in double buffering
  ref: any,                  // ref reference
};

The combination of these four dimensions allows a Fiber node to play four roles simultaneously:

Dimension Corresponding Fields Question Answered
Component Description tag, key, elementType, type, stateNode "Who am I?"
Tree Structure return, child, sibling, index "Where am I?"
Work State pendingProps, memoizedProps, memoizedState, updateQueue, lanes "What do I need to do?"
Side Effect Tracking flags, subtreeFlags, deletions, alternate "Who do I notify when I'm done?"

Every field is a necessary carrier for some runtime function. Delete any field, and some React function will break. Delete alternate, double buffering is gone. Delete lanes, concurrent scheduling is gone. Delete flags, DOM updates don't know what to do.


3. Understanding Each Field from the Source Code

3.1 ReactWorkTags.js — Identity Cards for 32 Types of Fibers

First, look at the tag field. It's not just any number, but an enum from ReactWorkTags.js:

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const HostRoot = 3;           // Tree root node
export const HostPortal = 4;         // Portal subtree entry
export const HostComponent = 5;      // DOM element (div, span...)
export const HostText = 6;           // Text node
export const Fragment = 7;
export const Mode = 8;               // StrictMode / ConcurrentMode
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostHoistable = 26;     // Hoistable resource (link preload)
export const HostSingleton = 27;     // Singleton element (html, head, body)
export const IncompleteFunctionComponent = 28;
export const Throw = 29;             // Special component for throwing errors
export const ViewTransitionComponent = 30;
export const ActivityComponent = 31;

32 types. From the most common FunctionComponent (0) to Throw (29) specifically for error handling, from HostComponent (5) representing DOM elements to DehydratedFragment (18) related to server-side rendering activation. Each type has its own processing logic in beginWork and completeWork.

The tag field is the reconciler's "routing table." When beginWork processes a Fiber node, the first judgment is switch (fiber.tag)—different tags take different branches. Function components need to call the function to get child nodes, class components need to call the render method, Host components need to create DOM elements, Suspense components need to check if they have resolved... Routing decisions are entirely based on this small integer.

3.2 FiberNode Constructor — The Birthplace of Over Twenty Fields

Look at the FiberNode constructor in ReactFiber.js:

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiber.js
function FiberNode(tag, pendingProps, key, mode) {
  // --- Instance ---
  this.tag = tag;
  this.key = key;
  this.elementType = null;    // ReactElement.type (unresolved raw value)
  this.type = null;           // Resolved value (function ref/class constructor/DOM tag string)
  this.stateNode = null;      // Associated real platform instance

  // --- Fiber (Linked List Pointers) ---
  this.return = null;         // Parent Fiber
  this.child = null;          // First child Fiber
  this.sibling = null;        // Next sibling Fiber
  this.index = 0;             // Position index in parent's children
  this.ref = null;            // ref reference
  this.refCleanup = null;     // ref cleanup function (useImperativeHandle, etc.)

  // --- Input / Output ---
  this.pendingProps = pendingProps;  // New props
  this.memoizedProps = null;         // Props used in the last render
  this.updateQueue = null;           // Update queue (state, callbacks)
  this.memoizedState = null;         // State used in the last render
  this.dependencies = null;          // Context dependency list

  // --- Mode ---
  this.mode = mode;           // ConcurrentMode / StrictMode / NoMode

  // --- Effects ---
  this.flags = NoFlags;               // Side effects this node needs to execute
  this.subtreeFlags = NoFlags;        // Side effects in the subtree that need to be executed
  this.deletions = null;              // List of child nodes to be deleted

  // --- Scheduling ---
  this.lanes = NoLanes;       // Priority of work this node needs to process
  this.childLanes = NoLanes;  // Priority of work the subtree needs to process

  // --- Alternate (Double Buffering) ---
  this.alternate = null;      // Points to the corresponding node in the other tree

  // --- Profiler (DEV mode) ---
  if (enableProfilerTimer) {
    this.actualDuration = 0;
    this.actualStartTime = -1;
    this.selfBaseDuration = 0;
    this.treeBaseDuration = 0;
  }
}

This code is surprisingly simple. No inheritance chain, no complex initialization logic—just a function, a bunch of this.xxx = yyy. But it's precisely this simplicity that gives Fiber nodes extremely high flexibility—it's a pure data object that can be arbitrarily copied, modified, discarded, and rebuilt without worrying about hidden side effects.

Note the difference between elementType and type—these two fields often cause confusion:

Field Value (Function Component) Value (Class Component) Value (DOM Element)
elementType Function ref (App) Class constructor (MyComponent) String ('div')
type Same as above Same as above Same as above

In most cases, they are equal. But in ForwardRef and Lazy components, elementType and type differ—elementType is the outer wrapper (REACT_FORWARD_REF_TYPE), and type is the inner real component. The Reconciler uses elementType to determine if a re-render is needed (comparing key and type), and uses type to execute the actual component call.

3.3 Linked List Pointers — child / sibling / return

These three fields are the soul of the Fiber architecture. They transform a hierarchical component tree into a linked list structure that can be traversed, interrupted, and resumed.

child: Points to the first child node. If a component has multiple child nodes, you can only find the first one through child, then traverse the rest through the sibling chain.

sibling: Points to the next sibling node. The chain child → sibling → sibling → null completely represents all child nodes of a parent.

return: Points to the parent node. Why is it called return and not parent? Because it's named from the traversal perspective—after processing the current node, which node to "return to" to continue processing. In depth-first traversal, after processing a leaf node, you need to return to its parent, then check the parent's sibling. return expresses this semantic more precisely than parent.

The combination of these three pointers allows the Fiber tree to express arbitrarily complex nested structures—but traversing it doesn't require recursion, just a while loop and a current pointer. The "position information" during traversal is completely stored in the object graph, not on the call stack. This is the fundamental reason why Fiber can be interrupted and resumed.

3.4 lanes and childLanes — The Pipeline for Priority Propagation

lanes and childLanes are the core fields of Fiber's concurrent scheduling. They are both 31-bit integers, using bitmasks to represent different update priorities.

Why is childLanes needed? Because the workLoop needs to quickly determine "is there work to do in a subtree." Without childLanes, the workLoop would need to traverse the entire subtree to determine if there's pending work—this is unacceptable in large applications. With childLanes, the workLoop only needs to check the root node's childLanes—if it's 0, the entire tree has no work; if it's non-zero, follow the branches marked with priority downwards.

Counter calls setState, Counter.lanes is marked as DefaultLane. This mark propagates upwards—Counter.return (App)'s childLanes is marked, App.return (HostRoot)'s childLanes is also marked. When the scheduler checks for work, it sees HostRoot.childLanes !== 0 and knows there's work in the subtree. But it doesn't need to check the Header branch—because App.childLanes only marks the branch where Counter is, Header's childLanes is 0 and can be safely skipped.

3.5 flags and subtreeFlags — Collectors of Side Effects

flags and subtreeFlags are the core of Fiber's side effect system. They are also bitmasks, each bit representing a type of side effect:

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js
export const NoFlags = /*                      */ 0b000000000000000000000000000;
export const Placement = /*                    */ 0b000000000000000000000000010;
export const Update = /*                       */ 0b000000000000000000000000100;
export const PlacementAndUpdate = /*           */ 0b000000000000000000000000110;
export const Deletion = /*                     */ 0b000000000000000000000001000;
export const ChildDeletion = /*                */ 0b000000000000000000000010000;
export const ContentReset = /*                 */ 0b000000000000000000000100000;
export const Callback = /*                     */ 0b000000000000000000001000000;
export const DidCapture = /*                   */ 0b000000000000000000010000000;
export const ForceClientRender = /*            */ 0b000000000000000000100000000;
export const Ref = /*                          */ 0b000000000000000001000000000;
export const Snapshot = /*                     */ 0b000000000000000010000000000;
export const Passive = /*                      */ 0b000000000000000100000000000;
export const Visibility = /*                   */ 0b000000000000001000000000000;
export const StoreConsistency = /*             */ 0b000000000000010000000000000;
export const HostEffectMask = /*               */ 0b000000000000011111111111111;
export const Hydrating = /*                    */ 0b000000000000100000000000000;
export const BeforeMutationMask = /*           */ 0b000000000000010000000100100;
export const MutationMask = /*                 */ 0b000000000000001010000010110;
export const LayoutMask = /*                   */ 0b000000000000000001000001000;
export const PassiveMask = /*                  */ 0b000000000000000100000000000;
Flag Meaning
Placement This is a new node that needs to be inserted into the DOM
Update The node's props or state have changed and need updating
Deletion The node needs to be deleted
ChildDeletion A child node has been deleted (used for cleaning up refs)
Ref The node's ref needs updating (attach or detach)
Snapshot The class component's getSnapshotBeforeUpdate needs to be called
Passive The useEffect callback needs to be executed
Visibility The visibility of the Offscreen component has changed

flags marks the side effects of this node. When beginWork processes a Fiber node, if it finds it needs to be inserted (newly created node) or updated (props changed), it sets the corresponding bit on the flags field.

subtreeFlags marks the bitwise OR of all side effects in the subtree. When completeWork "completes" a node (i.e., all its child nodes have been processed), it merges the child nodes' flags and subtreeFlags into its own subtreeFlags. This way, when the traversal reaches the root node, subtreeFlags contains all side effect types of the entire tree—the commit phase only needs to check the root node's subtreeFlags to decide which phases of side effect processing need to be executed.

The deletions field is a special array. When a child node needs to be deleted, it doesn't mark Deletion on its own flags—instead, it's placed into the parent node's deletions array. Why? Because deleted nodes won't participate in subsequent traversals; if the side effect mark is on itself, the commit phase might not find it. Placing it in the parent node's deletions array ensures the commit phase can reliably execute cleanup operations.

3.6 alternate — The Bridge of Double Buffering

alternate is the core field of Fiber's double buffering mechanism. Each Fiber node has an alternate, pointing to the corresponding node in the other tree (current or workInProgress).

When React starts a new render, it starts from the root node of the current tree and creates the workInProgress tree through createWorkInProgress. During this process, each newly created workInProgress Fiber is bidirectionally linked with the current Fiber through the alternate field:

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiber.js
workInProgress.alternate = current;
current.alternate = workInProgress;

The benefits brought by the alternate link are huge:

  1. Comparing old and new state: The render phase can get the props from the last render at any time through workInProgress.alternate.memoizedProps to decide if a re-render is needed.
  2. Reusing nodes: createWorkInProgress prioritizes reusing alternate nodes, reducing object creation and garbage collection pressure.
  3. Fast switching: The commit phase only needs to swap the root node's pointers, current becomes workInProgress, workInProgress becomes the old current. An atomic operation, no intermediate state.

4. Root Node: The Difference Between FiberRootNode and RootFiber

I always had a question: What exactly is the "root" created by createRoot(container)? Why are there two concepts of "root"?

React's "root" is actually a two-layer structure:

FiberRootNode (root container): Created by createFiberRoot. It is not a Fiber; it's an independent object type. It manages the global state of the entire React application—containerInfo (container DOM node), pendingLanes (all pending priorities), callbackNode (scheduling callback), onUncaughtError / onCaughtError (error handling callbacks). A React application usually has only one FiberRootNode.

RootFiber (root Fiber): A Fiber node with tag = HostRoot. It is the entry point of the Fiber tree—all components are its descendants. It has an alternate and participates in double buffering. RootFiber's stateNode points to FiberRootNode, and FiberRootNode's current points to the RootFiber of the current tree.

Why two layers? Because FiberRootNode does not participate in Fiber tree traversal and double buffering. Its fields (like pendingLanes, callbackNode) are global and don't need "two versions." If all fields were placed on RootFiber, these global states would need to be copied during double buffering, which wastes memory and is error-prone. The layered design puts "global state" and "tree nodes" in their respective places.

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js
export type FiberRoot = {
  // Container info
  tag: RootTag,                          // LegacyRoot / ConcurrentRoot
  containerInfo: Container,              // DOM container node
  // Tree pointer
  current: Fiber,                        // Points to the RootFiber of the current tree
  // Scheduling state
  callbackNode: mixed,                   // Scheduler's callback handle
  callbackPriority: Lane,                // Priority of the current callback
  pendingLanes: Lanes,                   // All pending lanes
  expiredLanes: Lanes,                   // Expired lanes (must execute synchronously)
  // Event handling
  onUncaughtError: (error: mixed, errorInfo: {...}) => void,
  onCaughtError: (error: mixed, errorInfo: {...}) => void,
  onRecoverableError: (error: mixed, errorInfo: {...}) => void,
  // ... more fields
};

5. Designing a Core Data Structure for a Complex Runtime

Bitmasks are a weapon for performance optimization

Fiber's lanes, flags, subtreeFlags are all bitmasks. Although bitwise operations in JavaScript are limited to 32 bits, for expressing "multiple-choice states" (a node can have multiple side effects, multiple priorities), bitmasks are much more efficient than arrays or objects:

These operations are all O(1) and can be highly optimized in modern JavaScript engines.

Good Data Structures Enable Teams to Work in Parallel

The separation of Fiber's four identities—component description, tree structure, work state, side effect tracking—allows different team members to focus on different areas:

These fields, defined together in the Fiber type, constitute the team's shared contract. Everyone works on the same data structure, but with different focuses.

React Fiber's Design Migration Strategy
Four-identity separation (Component/Tree/Work/Side Effects) Separate core data entity field groups by responsibility dimension, each team responsible for one group
Bitmasks for multi-choice states Manage states and flags with bitwise operations, reducing array traversal and object nesting
childLanes propagate upwards Use aggregate fields to avoid full traversal when needing to "quickly determine if a subtree has work" in tree structures
alternate double buffering Maintain parallel structures of two versions for any scenario requiring "prepare a new version then atomically switch"
FiberRootNode and RootFiber layering Separate global state from tree node state to avoid copying global data during double buffering

6. Over Twenty Fields, Supporting a World

Back to that FiberNode constructor.

function FiberNode(tag, pendingProps, key, mode) {
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  this.ref = null;
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.memoizedState = null;
  this.updateQueue = null;
  this.dependencies = null;
  this.mode = mode;
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  this.alternate = null;
}

Over twenty fields. No magic, no black technology. But their combination supports one of the most complex runtime systems in modern frontend frameworks.

tag determines "who I am." child / sibling / return determine "where I am and who I'm connected to." pendingProps / memoizedState / updateQueue determine "what I need to do." lanes / childLanes determine "when to do it." flags / subtreeFlags / deletions determine "who to notify after it's done." alternate determines "how to recover to the previous state if interrupted."

The combination of these fields allows React to: interleave multiple updates of different priorities within a 16-millisecond frame gap; respond to user input during rendering without losing position information; safely discard incomplete work before commit; call lifecycle methods after DOM operations are complete.

A good data structure is like a good building structure—it's not conspicuous, but it determines the possibilities and upper limits of the entire system. The Fiber data structure is React's skeleton. Hooks, Concurrent Mode, Suspense, Server Components—these magnificent superstructures all stand on the shoulders of these twenty-plus fields.