Deep dive into React Fiber

This article aims to provide a clear understanding of React Fiber and its influence on front-end development. However, before we dive into React Fiber and explore its purpose and problem-solving capabilities, it's essential to grasp the role of Object-Oriented Programming (OOP) in React.

Object Oriented Programming in React

Object-Oriented Programming (OOP) encompasses four key principles, often referred to as the "four major pillars":

  1. Abstraction:

    • This pillar allows users to interact with code or features without needing to understand their inner workings. It provides a simplified interface for complex functionalities.
  2. Encapsulation:

    • Encapsulation involves bundling specific information, data, or data members into a self-contained unit, making them reusable, and maintaining data integrity. It keeps the details hidden and provides controlled access.
  3. Polymorphism:

    • "Poly" means many, and "morphs" relates to form or structure. Polymorphism allows objects to take on different forms or behaviors, depending on the context. It promotes flexibility by enabling one interface to be used for a general set of actions.
  4. Inheritance:

    • Inheritance involves acquiring properties and behaviors from a parent object or class. It facilitates code reusability and the creation of specialized classes that inherit common attributes and methods from their parent, promoting a hierarchical structure.

This article mainly discusses React Fiber, with a specific focus on the key OOPs concept crucial to React Fiber, which is Abstraction. You've likely encountered the following code snippet frequently when working with React.

What's truly remarkable about React is that, despite the complex algorithms used to enhance the user experience, developers using React for front-end applications don't need to concern themselves with the implementation details. The image below piques my curiosity about how React achieves this.

The intriguing observation is that the left side of the triangle doesn't offer as smooth a user experience as the right side. This disparity is due to the Frame drop effect, which is closely tied to the behavior of the human eye's retina. We know that all videos, across all platforms, consist of multiple images displayed at 30 frames per second (FPS) or even fewer. Anything exceeding this frame rate leads to an improved experience, thanks to reduced latency.

What is a Frame?

The frame can be thought of as the time interval allocated to display N images in one second. To ensure a more pleasing experience for the human eye, N should be less than or equal to 30.

A clear oldest example of it could be flipbooks.

The reason for discussing frames and minimizing frame drop effects is straightforward. These factors are essential for achieving a superior user experience in web development, whether it involves state manipulation in a content management system, creating interactive dashboards with graphs, or implementing animation effects on a brand's platform.

In my view, React represents a groundbreaking advancement in web development. The transition this library underwent from React 16.8 onwards is truly remarkable. A pivotal aspect of this significant leap was the adoption of a new Reconciler algorithm, replacing the older Stack Reconciler algorithm.

Stack Reconciler algorithm

Two crucial terms to pay attention to are:

Stack: A data structure that follows a "last in, first out" (LIFO) philosophy, where the last item added is the first to be removed.

Reconciler: The process of re-evaluating and recreating components, often used in the context of rendering and updating in React.

Before React version 16, it utilized the stack reconciler algorithm, which is rooted in the stack concept. You might wonder how a stack relates to manipulating the state of the DOM tree. The connection lies in how the DOM manipulation is achieved through recursive calls. When recursion is involved, everything centers around the memory stack.

However, before delving deeper, let's first explore React's role in state manipulation, considering that JavaScript can manage this independently using its memory stack.

In this context, you can think of React as a mentor that guides your vanilla JavaScript code to leverage the memory stack for optimal performance.

The algorithm employed to handle frame drops and display updated states on the DOM had some performance issues, causing a slight lag. Consequently, a more efficient solution was sought, although it still had room for enhancement.

Graphical analysis of Stack Reconciler & React Fiber

The issue here is quite evident, particularly with hinges 1, 2, and 3. These hinges differ from the larger, incomplete DOM element, which causes a delay in updating the most recent state to reach the top React element node. This delay results in frame drops and a suboptimal user experience.

The improved solution entails achieving an immediate update on the DOM. However, this can't be accomplished without introducing some form of asynchronous operation. This line of thinking gave rise to React Fiber, named after the Fiber data structure.

For a more in-depth exploration of the Fiber data structure, you can refer here.

The solution was already in existence; what was required was a comprehensive analysis and successful implementation. Now, let's examine the graphical representation of React Fiber.

The image presented above may appear to solve all the issues, but it's not quite that simple. The reason for this assertion lies in the fact that we are dealing with 60 frames per second (FPS), which translates to a mere 1/60 millisecond for each frame. Maintaining a consistent speed of 16.67 milliseconds for one frame requires an intelligent mechanism to support the implementation of the Fiber data structure. This is where the Virtual DOM steps in to assist React Fiber.

Before we proceed further, it's essential to emphasize that the React Fiber algorithm already has an implementation work loop designed to manage this tight timeframe. The Virtual DOM acts as a catalyst in this process.

Virtual DOM & React Fiber in the same picture

React Fiber benefits from assistance to optimize its performance. When React Fiber observes how it operates, it pauses and reschedules another task before returning to the previous one if there's any remaining time. By providing this mechanism with reusable content, the process becomes more efficient.

It's crucial to understand that the concept of the Virtual DOM involves creating a duplicate of the original DOM. This duplication serves as a backup option, allowing us to use previous information about the DOM from the Virtual DOM object rather than starting from scratch.

In a concise manner, what is Fiber in React?

In React, Fiber is essentially a straightforward JavaScript object that contains the properties of a React Element, and it serves as a unit of work. To grasp the concept better, let's delve into what a React Element is.

A React element is a basic JavaScript object that describes a React component. It includes details such as properties and children.

The React element has a connection to the DOM element, which is the deeper level where the life of any component begins.

To put it plainly, DOM elements give rise to React Elements, and React Elements, in turn, give birth to React components.

DOM manipulation, Execution context & React

The immediate question that arises is how these three distinct topics are interconnected. The answer can be found in the implementation of execution context and asynchronous operations.

Image credit : Credit: Lydia Hallie

In a brief overview, the Event loop and Execution context work as follows: When JavaScript code runs, a global execution context is established. Synchronous operations are managed by the main thread's call stack. Asynchronous operations wait in Web APIs, depending on their type. When the event loop detects an empty call stack, it selects the highest priority task from the respective queue and adds it to the call stack, allowing for function execution in JavaScript.

React Fiber's approach mirrors this operation but with the asynchronous nature of React. Tasks in React Fiber don't execute immediately for DOM updates; instead, they initially wait in a queue. Depending on their priority, they are then moved to the call stack using the requestIdleCallback() function.

Browsers that support requestIdleCallback() can seamlessly manage the transition from the queue to the call stack. For browsers that do not support this function, React provides a corresponding polyfill to ensure compatibility.

This dual approach in handling code arises because React is responsible for more than just DOM manipulation; it also handles other operations that aren't contained within the render or return functions.

However, there's a notable difference. When observing React Fiber's sinusoidal behavior, we see that execution moves from the updated part at the smallest level of the DOM to the top-most DOM node within a given time frame. If it completes the task within this timeframe, the specific task is done. If not, asynchronous handling takes place. If the task remains incomplete, React Fiber reverts to the stack while retaining the previous information, making it easier to handle fallback scenarios.

Constituents or properties of React Element & React Fiber object

A React element node serves as an instance of a React component in the form of a regular JavaScript object. The primary properties of this JavaScript object include:

React element = {
    type,
    key,
    child,
    sibling,
    return,
    alternate,
    output
}
  1. Type: <div>, <span> or class/function component

  2. Key: Unique ID

  3. Child: the value you get when you execute render() of the respective RF

  4. Siblings: [<Component1/>, <Component2/>, <Component3/>]

  5. Return: This identifies when the current frame will end and where the control will fall back.
    This is identified by 2 major properties:

    • pendingProps()

    • memoizedProps()

Note:

React Fiber primarily consists of one property, which is the stateNode.

React Fiber = { stateNode }

The stateNode property maintains a one-to-one relationship between a React element and the corresponding DOM node element, utilizing the five properties of the React element mentioned earlier.

Understanding the interconnection of React elements with one another is pivotal as it plays a crucial role in updating the state of the DOM.

The scenario outlined here is quite simple: squaring a number when a button is clicked. The relationship between the components involved in this scene is depicted as follows:

It's important to recognize the relationships in this scenario. Initially, there's an intra-relationship between the list elements (considered as the <List/> component) and the button. However, immediately after that, the relationship shifts from the button to each individual item (1, 2, 3 in separate <div/> elements). This change in relationship is then communicated to both the parent Item and the main <List/> component. The transmission of state information via setState() from the main component to Item and back to <List/> represents synchronous behavior.

1^2 ===> no change ===> leads to no state update

2^2 ===> changed to 4 ===> leads to state update

3^2 ===> changed to 9 ===> leads to state update

Now to make things more visible regarding React Fiber’s behavior, let’s add one more operation & which is increasing the size of the squared number.

When we would click on the increase in size button, that would put the item’s update in the effect list.

Effect list can be considered like a callback queue where the updation of the item would occur & get executed depending upon the priority.

Note:

The starred items are the only ones that will face the state update i.e. complete list component and the div values of 2 & 3 will change only because of the value changing to 4 & 9.

Now the question comes as to how will all these starred items will change along with the changes caused by the change in size button. So the answer lies in the prioritization of tasks in the effect list.

Now, all the above changes get executed in Phase-1 i.e. the Render phase. Wait, what is the phase?

Phases

As observed earlier, the state update process is not instantaneous; it occurs in two distinct phases, known as:

Phase-1 or Render phase: The render phase handles all logical changes related to the state update on the Virtual DOM of the respective component. React clones the items that don't require changes and updates only the necessary ones.

Phase-2 or Commit phase: The commit phase is responsible for applying all the changes from the Virtual DOM to the actual DOM, ensuring that the updated state becomes visible.

Commit phase impact

Before moving ahead with what’s happening because of the commit phase, we need to analyze what’s happening within the commit phase.

In the context of the diagram provided, the Work-in-Progress tree contains the latest changes, while the Current tree becomes obsolete. At this point, the pointers are swapped, and this transition is where features like Error Boundaries come into play.

Before the introduction of Error Boundaries, if a defect was encountered in the React DOM element tree, the entire tree would collapse, resulting in the user seeing only a blank page. This was detrimental to both user experience (UX) and security. However, Error Boundaries now prevent such scenarios by providing a backup tree.

Lifting the updated state to the top Fiber Node

The updates we've witnessed due to React Fiber's behavior have primarily addressed the issue of achieving faster DOM updates, aiming for or exceeding 60 FPS. However, there is still a challenge with state updates, specifically the reordering of the most updated DOM node to the top.

To resolve this issue, React Fiber follows a priority list, as outlined below:

  1. Synchronous function call

  2. Task (synchronous) immediately following the previous task

  3. Animation

  4. High priority

  5. Low priority

  6. Offscreen tasks (not directly related to DOM manipulation, such as API calls or I/O operations)

This priority list ensures that tasks are executed in an organized manner, prioritizing critical updates and maintaining a smooth user experience.

Disrupted prioritization

Prioritization can also involve asynchronous behavior. For instance, if a low-priority task is currently in progress, and a high-priority task becomes immediately relevant, the low-priority task can be preempted, and the previous Fiber created for it will be re-implemented for the new high-priority task. This dynamic prioritization ensures that higher-priority tasks receive timely attention, even if lower-priority tasks were previously underway.

The key point to take note of is that the previous low-priority task was temporarily paused to allow the high-priority task to be completed, using an optimization approach that reuses the previously created thread.

This concept revolves around breaking down larger tasks into smaller ones and prioritizing them based on the previously discussed priority list. You might have encountered this approach while working with React in development or production.

Indeed, this concept aligns with the practice of Lazy Loading. In lazy loading, we employ a similar mechanism where we divide a significant task into smaller chunks, ensuring that only the required files or static content are loaded onto the DOM for improved speed.

Let's relate this concept to the field of operating systems through Cooperative Scheduling. The term is quite self-explanatory; we divide tasks into smaller chunks and prioritize them according to the priority list, all while making effective use of previously executed low-priority tasks. This approach enhances task management and performance, similar to how lazy loading optimizes content delivery.

The division of tasks raises two important questions:

1. Consistency: Consistency can be upheld across all task segments or chunks through a concept known as Parallelism. By introducing more workers to the task, React can efficiently work with each worker at every level of the tree. This approach enables React to process tasks swiftly, maintaining consistency, and providing continuous feedback to the upper levels to ensure it remains updated with the latest changes.

2. Starvation Situation: The risk of a starvation situation arises when low-priority tasks are constantly pushed back by high-priority tasks, causing them to wait indefinitely. To mitigate this, React employs mechanisms to ensure that low-priority tasks eventually get their turn and are not permanently starved.

The starvation situation, so when a high-priority task is always given priority then, chances are there the low-priority task would not get into the picture and they would be always in a starved state.

The data structure in the React Fiber state update

React components are organized in a tree structure, and the traversal of this tree occurs recursively, following a depth-first search (DFS) approach. However, unlike the earlier stack implementation, React Fiber utilizes a singly linked list, as we observed in the effect queue. Each fiber node contains the following properties:

Type, key, child, sibling, return, alternate, output

These data structures help us to use the mechanism of Pause, Resume, Reuse, and create to bring the most efficient possible so far, which is termed React Fiber.

To, get exact information about the functions involved in the above process of Pause, Resume, Reuse & Create, you may refer to the official repository.

Acknowledgment

This article is the result of my thorough examination of various articles and analyses related to React Fiber and its associated concepts.

I would like to extend my special thanks to: