Slower, smoother: dealing with React Fiber


September 16, 2017 React Fiber was released - a new major version of the library. In addition to adding new features that you can read about here , the developers have rewritten the architecture of the library core. As a React developer, I decided to figure out what kind of beast this Fiber is, what tasks it solves, due to which and how, in the end, I can apply the acquired knowledge to projects that I work on at Live Typing . Understood and came to mixed conclusions.


Stack vs Fiber


To understand what has changed in the new architecture, you need to understand the shortcomings of the old. For example, consider the following demo:



We have two components, the source code of which you can see here. The first component runs on the old version of the architecture, which was called Stack, the second - using Fiber. The difference is noticeable to the naked eye: the animation of the second component works much smoother than the animation of the first.


What causes a delay in the animation of a component implemented on Stack? Let's open the Performance tab in the browser and look at the Frames field, as well as the runtime of the SierpinskiTriangle function (by which we mean the render method of the SierpinskiTriangle component). In this function, the process of comparing the old and the new virtual tree takes place. The speed of the frame change depends on how quickly this process is performed. In this case, it equals 700 ms, and this is a long time.


Stack
Figure 1. Component operation on the Stack core


From here we can conclude that the main problem of the old architecture was the long execution of the render method of the SierpinskiTriangle component. It would hardly have been possible to accelerate it due to some optimization of the algorithm itself.


Figure 2 illustrates how React on a Fiber core renders a component. We see that the frames change with a frequency of once every 17 ms. Roughly speaking, Fiber somehow breaks up a function that takes a long time to small functions that are fast.


Fiber
Figure 2. Component operation on the Fiber core


Fiber in theory


How does Fiber split a function into parts? To do this, it is necessary to manage the process of performing this function, as well as provide the opportunity:


  • prioritize different types of work;
  • stop work;
  • interrupt work if it is no longer needed;
  • use previous calculations.

To implement the above, we need to determine how to divide the work comparing the old and new DOM trees into parts.


If you look at React, then all the components in it are functions. And rendering a React application is a recursive function call from the youngest component to the oldest. We have already seen that if the function of changing our component works out for a long time, then a delay occurs. To solve this problem, we can use two methods that provide browsers:


  1. requestIdleCallback, which allows you to perform low priority calculations while the main browser thread is idle;
  2. requestAnimationFrame, which allows us to request the execution of our animation in the next frame.

As a result, the plan is this: we need to calculate part of the change in our interface for the requestIdleCallback event, and, as soon as we are ready to draw the component, request requestAnimationFrame, in which it will happen. But it is still necessary to somehow interrupt the execution of the function of comparing virtual trees and at the same time maintain intermediate results. To solve this problem, React developers decided to develop their own version of the call stack. Then they will have the opportunity to stop the execution of functions, independently give priority to the execution of functions that need it more, and so on.


Reactivation of the call stack within the framework of React-components is the new Fiber algorithm. The advantage of reimplementing the call stack is that you can store it in memory, stop it, and start it when you need it.


Fiber in Practice: Finding the Fibonacci Number


Standard search implementation


The implementation of the Fibonacci retrieval using the standard call stack can be seen below.


function fib(n) {
  if(n <= 2) {
    return 1;
  } else {
    var a = fib(n - 1);
    var b = fib(n - 2);
    return a + b;
  }
}

First, let’s see how the Fibonacci number search function is performed on a regular call stack. As an example, we will look for the third number.


So, a stack frame is created in the call stack, in which local variables and function arguments will be stored. In this case, the stack frame will initially look like this:



Because n> 2, then we get to the next line:


function fib(n) {
  if(n <= 2) {
    return 1;
  } else {
    var a = fib(n - 1); // мы находимся здесь
    var b = fib(n - 2);
    return a + b;
  }
}

Here the fib function will be called again. A new stack frame will be created, but n will be one less already, that is 2. Local variables will still be undefined.



And because n = 2, then the function returns one, and we return back to line 5


function fib(n) {
  if(n <= 2) {
    return 1;
  } else {
    var a = fib(n - 1); // а теперь здесь
    var b = fib(n - 2);
    return a + b;
  }
}

The call stack looks like this:



Next, the Fibonacci retrieval function is called for variable b, line 6. A new stack frame is created:


function fib(n) {
  if(n <= 2) {
    return 1;
  } else {
    var a = fib(n - 1);
    var b = fib(n - 2); // мы находимся здесь
    return a + b;
  }
}

The function, as in the previous case, returns 1.


The stack frame looks like this:



After which the function returns the sum of a and b.


Implementing a search on Fiber


Disclaimer: In this case, we have shown how the search for the Fibonacci number is performed with the reimplementation of the call stack. A similar method is implemented by Fiber.


function fiberFibonacci(n) {
  var fiber = { arg: n, returnAddr: null, a: 0 /* b is tail call */ };
  rec: while (true) {
    if (fiber.arg <= 2) {
      var sum = 1;
      while (fiber.returnAddr) {
        fiber = fiber.returnAddr;
        if (fiber.a === 0) {
          fiber.a = sum;
          fiber = { arg: fiber.arg - 2, returnAddr: fiber, a: 0 };
          continue rec;
        }
        sum += fiber.a;
      }
      return sum;
    } else {
      fiber = { arg: fiber.arg - 1, returnAddr: fiber, a: 0 };
    }
  }
}

Initially, we create a variable fiber, which in our case is a stack frame. arg is the argument to our function, returnAddr is the return address, a is the value of the function.


Because fiber.arg in our case is 3, which is more than 2, then we go to line 17,


function fiberFibonacci(n) {
  var fiber = { arg: n, returnAddr: null, a: 0 /* b is tail call */ };
  rec: while (true) {
    if (fiber.arg <= 2) {
      var sum = 1;
      while (fiber.returnAddr) {
        fiber = fiber.returnAddr;
        if (fiber.a === 0) {
          fiber.a = sum;
          fiber = { arg: fiber.arg - 2, returnAddr: fiber, a: 0 };
          continue rec;
        }
        sum += fiber.a;
      }
      return sum;
    } else {
      fiber = { arg: fiber.arg - 1, returnAddr: fiber, a: 0 }; // строка 17
    }
  }
}

where we create a new fiber (stack frame). In it, we store a link to the previous frame of the stack, the argument is one less and the initial value of our result. Thus, we recreate the call stack that we created when we recursively call the usual Fibonacci number search function.


Then we iterate over our stack in the opposite direction and count our Fibonacci number. lines 7-15.


var sum = 1;
      while (fiber.returnAddr) {
        fiber = fiber.returnAddr;
        if (fiber.a === 0) {
          fiber.a = sum;
          fiber = { arg: fiber.arg - 2, returnAddr: fiber, a: 0 };
          continue rec;
        }
        sum += fiber.a;
      }
      return sum;

Conclusion


Is React faster after implementing Fiber? According to this test , no. It became even slower by about 1.5 times. But the introduction of the new architecture made it possible to more efficiently use the main stream of the browser, due to which the work of animations became smoother.


Also popular now: