javascript

Медленнее, плавнее: разбираемся с React Fiber

  • четверг, 30 ноября 2017 г. в 03:13:05
https://habrahabr.ru/post/343504/
  • ReactJS
  • JavaScript



16 сентября 2017 года вышла React Fiber — новая мажорная версия библиотеки. Помимо добавления новых фич, о которых вы можете почитать здесь, разработчики переписали архитектуру ядра библиотеки. Я как React-разработчик решил разобраться, что за зверь этот Fiber, какие задачи он решает, за счёт чего и как в итоге можно применить полученные знания на проектах, над которыми я тружусь в компании Live Typing. Разобрался и пришёл к неоднозначным выводам.


Stack против Fiber


Чтобы понять, что поменяли в новой архитектуре, нужно разобраться в недостатках старой. Для примера рассмотрим следующее демо:


  • приоритизировать разные типы работы;
  • останавливать работу;
  • прерывать работу, если она больше не нужна;
  • использовать предыдущие расчёты.

Для реализации вышеперечисленного нам необходимо определить, как разделить работу по сравнению старого и нового DOM деревьев на части.


Если посмотреть на React, то все компоненты в нём являются функциями. А отрисовка React-приложения — это рекурсивный вызов функций от самого младшего компонента до старшего. Мы уже видели, что, если функция изменения нашего компонента долго отрабатывает, то возникает задержка. Для решения данной проблемы мы можем воспользоваться двумя методами, которые предоставляю браузеры:


  1. requestIdleCallback, который позволяет выполнять расчёты с малым приоритетом, пока главный поток браузера простаивает;
  2. requestAnimationFrame, которая позволяет сделать запрос на выполнение нашей анимации в следующем фрейме.

В итоге план такой: нам нужно просчитать часть изменения нашего интерфейса по событию requestIdleCallback, и, как только мы будем готовы отрисовать компонент, запросить requestAnimationFrame, в котором это произойдёт. Но всё ещё необходимо как-то прервать выполнение функции сравнения виртуальных деревьев и при этом сохранять промежуточные результаты. Для решения этой проблемы разработчики React решили разработать свою версию стека вызовов. Тогда у них будет возможность останавливать выполнение функций, самостоятельно давать приоритет выполнения функциям, которым он больше необходим, и так далее.


Реимплементации стека вызовов в рамках React-компонентов и есть новый алгоритм Fiber. Преимущество реимплементации стека вызовов в том, что вы можете хранить его в памяти, останавливать и запускать тогда, когда вам это необходимо.


Fiber на практике: поиск числа Фибоначчи


Стандартная реализация поиска


Реализацию поиска числа Фибоначчи с использованием стандартного стека вызовов можно увидеть ниже.


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

Сначала разберём, как выполняется функция поиска числа Фибоначчи на обычном стеке вызовов. В качестве примера будем искать третье число.


Итак, в стеке вызовов создаётся кадр стека, в котором будут храниться локальные переменные и аргументы функции. В данном случае кадр стека изначально будет выглядеть таким образом:



Т.к. n > 2, то мы дойдем до следующей строки:


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

Здесь вновь будет вызвана функция fib. Создастся новый кадр стека, но n будет уже на единицу меньше, то есть 2. Локальные переменные всё так же будут undefined.



И т.к. n=2, то функция возвращает единицу, а мы возвращаемся обратно на строку 5


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

Стек вызовов выглядит так:



Далее вызывается функция поиска числа Фибоначчи для переменной b, строка 6. Создаётся новый кадр стека:


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

Функция, как и в предыдущем случае, возвращает 1.


Кадр стека выглядит так:



После чего функция возвращает сумму a и b.


Реализация поиска на Fiber


Дисклеймер: В данном случае у нас показано, как исполняется поиск числа Фибоначчи с реимплементацией стека вызовов. Похожим способ реализован 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 };
    }
  }
}

Изначально у нас создается переменная fiber, который в нашем случае является кадром стека. arg — аргумент нашей функции, returnAddr — адрес возврата, a — значение функции.


Т.к. fiber.arg в нашем случае равен 3, что больше 2, то мы переходим на строку 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
    }
  }
}

где у нас создаётся новый fiber (кадр стека). В нём мы сохраняем ссылку на предыдущий кадр стека, аргумент на единицу меньше и начальное значение нашего результата. Таким образом, мы воссоздаём стек вызовов, который у нас создавался при рекурсивном вызове обычной функции поиска числа Фибоначчи.


После чего мы в обратную сторону итерируемся по нашему стеку и считаем наше число Фибоначчи. строки 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;

Вывод


Стал ли быстрее React после внедрения Fiber? Согласно этому тесту — нет. Он стал даже медленнее примерно в 1,5 раза. Но внедрение новой архитектуры дало возможность рациональнее пользоваться главным потоком браузера, за счёт чего работа анимаций стала плавнее.