javascript

Глубокое погружение в React Fiber

  • вторник, 25 августа 2026 г. в 00:00:07
https://habr.com/ru/companies/timeweb/articles/1068626/

Задумывались ли вы когда-нибудь о том, что на самом деле происходит под капотом, когда вы рендерите приложение React? Мы знаем, что React строит дерево DOM и отображает UI на экране, но как React конструирует это дерево и как он эффективно обновляет его при изменении состояния?

В этой статье мы рассмотрим React Fiber — основной движок согласования (reconciliation) React. Мы рассмотрим, как React строил DOM до v15, недостатки этой ранней стековой (stack) модели с точки зрения производительности и то, как асинхронная архитектура Fiber решает эти проблемы, начиная с React v16 и до React v19.

❯ Что такое React Fiber?

React Fiber — это внутреннее изменение движка, направленное на то, чтобы сделать React быстрее и умнее. Механизм согласования (reconciler) Fiber, ставший механизмом согласования по умолчанию для React 16 и выше, представляет собой полную переработку алгоритма согласования React для решения некоторых давних проблем.

Поскольку Fiber является асинхронным, React может:

  • приостанавливать, возобновлять и перезапускать рендеринг компонентов по мере поступления новых обновлений

  • использовать результаты ранее выполненной работы и даже прерывать ее, если ее выполнение больше не актуально

  • разделять работу на части и расставлять приоритеты задач в зависимости от их важности

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

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

❯ Что такое стековый механизм согласования?

Начнем со знакомого всем нам ReactDOM.render(<App />, document.getElementById('root')). Модуль ReactDOM передает <App /> механизму согласования, но здесь возникает два вопроса:

  1. На что ссылается <App />?

  2. Что такое механизм согласования?

Ответим на оба эти вопроса.

❯ Что такое ?

<App /> — это элемент React (React element), а «элементы описывают дерево». Согласно блогу React, «элемент — это обычный объект, описывающий экземпляр компонента или узел DOM, а также его свойства/пропы (props)».

Другими словами, элементы — это не настоящие узлы DOM или экземпляры компонентов; это способ сообщить React, какого вида элементы они представляют, какие свойства содержат и каковы их потомки (children).

В этом заключается реальная мощь React: React абстрагирует сложные части создания, рендеринга и управления жизненным циклом настоящего дерева DOM, делая жизнь разработчика более легкой.

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

❯ Почему React использует элементы вместо экземпляров DOM?

Элементы React позволяют разработчикам описывать желаемый интерфейс без ручного создания, обновления и уничтожения каждого объекта DOM.

В мире обычного объектно-ориентированного программирования разработчики должны инстанцировать и управлять жизненным циклом каждого элемента DOM. Например, чтобы создать простую форму и кнопку отправки от разработчика требуются некоторые усилия по управлению состоянием.

Допустим, компонент Button содержит переменную состояния isSubmitted. Жизненный цикл компонента Button представлен на диаграмме ниже, где каждое состояние управляется приложением:

Размер диаграммы и количество строк кода растет экспоненциально по мере роста числа переменных состояния.

React предоставляет элементы для решения этой проблемы; в React существует два вида элементов: элементы DOM и элементы-компоненты.

Элемент DOM — это элемент, который является строкой, например, <button class="okButton">OK</button>.

Элемент-компонент — это класс или функция, например, <Button className="okButton">OK</Button>, где <Button> — это классовый или функциональный компонент. Это типичные компоненты React, которые мы обычно используем.

Важно понимать, что оба типа — простые объекты. Они являются лишь описанием того, что должно быть отображено на экране, и не запускают рендеринг при их создании и инициализации.

❯ Что такое согласование?

Согласование (reconciliation) — это процесс сравнения деревьев компонентов для определения того, какие части результата рендеринга нуждаются в модификации.

Это упрощает React разбор и обход этих элементов для построения DOM-дерева. Фактическая отрисовка происходит позже, после завершения обхода (traverse).

Когда React встречает классовый или функциональный компонент, он «спрашивает» у него, какой элемент следует отобразить, исходя из его свойств.

Например, если компонент <App> рендерит следующее:

<Form>
  <Button>
    Submit
  </Button>
</Form>

React спросит компоненты <Form> и <Button>, что они рендерят на основе соответствующих свойств.

Если компонент Form является функциональным компонентом, который выглядит так:

const Form = (props) => {
  return(
    <div className="form">
      {props.form}
    </div>
  )
}

React вызовет метод render(), чтобы узнать, какие элементы он отображает, и увидит, что он отображает компонент <div> с потомком.

React будет повторять этот процесс до тех пор, пока не выяснит элементы DOM для каждого компонента на странице.

Этот процесс рекурсивного обхода дерева для определения базовых элементов DOM в дереве компонентов приложения React называется согласованием. К концу согласования React знает результат обхода DOM-дерева, и рендерер, например react-dom или react-native, применяет минимальный набор изменений, необходимых для обновления узлов DOM. Это означает, что при вызове ReactDOM.render() или setState(), React выполняет согласование.

В случае setState() выполняется обход дерева и определяется, что изменилось, путем сравнения нового дерева с отрисованным. Затем эти изменения применяются к текущему дереву, тем самым обновляя состояние, соответствующее вызову setState().

Заметьте: в этом разделе используется ReactDOM.render(), поскольку он отражает API на момент представления Fiber. Он был признан устаревшим в React 18 и удален в React 19. Современные приложения с клиентским рендерингом используют createRoot():

import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"));

root.render(<App />);

Рассмотрим недостатки такой модели согласования.

❯ Что такое стековый механизм согласования?

Стековый механизм согласования — оригинальная система согласования React. Она рекурсивно обрабатывала дерево компонентов, используя стек вызовов (call stack) JavaScript, и не могла приостанавливать работу после начала рендеринга. Ее название происходит от соответствующей структуры данных — стека, реализующего принцип «последним вошел — первым вышел» (LIFO).

У вас может возникнуть вопрос: «Какое отношение поведение стека имеет к тому, что мы только что рассмотрели?». Ну, поскольку мы используем рекурсию, это напрямую связано со стеком вызовов.

❯ Как рекурсия ограничивала старый механизм согласования?

Старый механизм согласования рекурсивно рендерил каждый компонент, а затем обходил их потомков до достижения листовых (конечных) элементов DOM.

Рассмотрим простой пример и посмотрим, что происходит в стеке вызовов:

function fib(n) {
  if (n < 2){
    return n
  }
  return fib(n - 1) + fib (n - 2)
}

fib(10)

Каждый вызов fib() помещается в стек до вызова fib(1), которая является первой функцией, выполняющей возврат (return).

Затем он продолжает помещать рекурсивные вызовы и удаляет их при достижении оператора return. Таким образом, он эффективно использует стек вызовов до тех пор, пока fib(3) не выполняет возврат и не становится последний элементом, удаляемым из стека.

Старый алгоритм согласования был чисто рекурсивным. Обновление приводило к незамедлительному повторному рендерингу всего поддерева.

Как отмечает Andrew Clark, в UI не обязательно сразу применять каждое обновление. В действительности это может впустую расходовать ресурсы, вызывать пропуск кадров (drop frames) и ухудшать пользовательский опыт.

Кроме того, разные типы обновлений имеют разный приоритет. Обновление анимации должно завершаться быстрее, чем обновление из хранилища данных.

❯ Что такое частота кадров и из-за чего подвисает UI?

Частота кадров (frame rate) — это частота, с которой на экране появляются последовательные изображения. Все, что мы видим на экранах компьютеров, состоит из изображений или кадров, воспроизводимых на экране с такой частотой, что смена кадров незаметна для глаза.

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

Как правило, для того, чтобы видео воспроизводилось плавно и незаметно для человеческого глаза, оно должно воспроизводиться с частотой, как минимум, 30 кадров в секунду (frames per second, FPS). Чем выше частота, тем лучше результат.

В настоящее время большинство устройств обновляют экраны с частотой 60 FPS, 1/60 = 16,67 мс. Это означает, что новый кадр отображается каждые 16 мс. Это число важно, потому что если рендереру React требуется более 16 мс для отрисовки чего-либо на экране, браузер пропускает этот кадр.

В действительности, однако, браузеру необходимо выполнить много внутренней работы, поэтому все ваши задачи должны быть завершены в течение 10 мс. Если вы не уложитесь в этот лимит, частота кадров снизится, и контент будет «дергаться» на экране. Это часто называют «заиканием» (jank), и это негативно сказывается на пользовательском опыте.

Разумеется, для статичного и текстового контента — это не проблема. Но в случае анимации это число критично.

Если алгоритм согласования обходит все дерево приложения и выполняет повторный рендеринг при каждом обновлении, и этот обход занимает больше 16 мс, браузер пропускает кадры.

Это одна из главных причин, почему многие хотели, чтобы обновления сортировались по приоритету, а не применялись вслепую. Кроме того, многие хотели иметь возможность приостанавливать и возобновлять работу в следующем кадре. Таким образом, React мог бы лучше контролировать распределение 16-миллисекундного бюджета рендеринга.

Это привело к переписыванию алгоритма согласования командой React. Новый механизм получил название Fiber. Рассмотрим, как Fiber решает эту проблему.

❯ Как React Fiber работает под капотом?

JavaScript обычно выполняет синхронные функции до тех пор, пока стек не опустеет. Старый механизм согласования React полагался на этот стек, поэтому он не мог приостановить рендеринг для того, чтобы браузер мог выполнить более срочную работу.

Движок JS создает контекст выполнения (execution context) для каждой функции. При запуске движок JS создает глобальный контекст выполнения, который содержит глобальные объекты, такие как window в браузере или global в Node.js.

JS обрабатывает оба контекста с помощью стековой структуры данных, также известной как стек выполнения (execution stack). Поэтому, когда мы пишем что-то вроде этого:

function a() {
  console.log("i am a")
  b()
}

function b() {
  console.log("i am b")
}

a()

Движок JS сначала создает глобальный контекст выполнения и помещает его в стек выполнения.

Затем он создает контекст выполнения для функции a(). Поскольку b() вызывается внутри a(), движок создает еще один контекст выполнения для b() и также помещает его в стек.

Когда b() возвращается, движок уничтожает контекст b(). Когда мы выходим из a(), ее контекст также уничтожается. Стек в процессе выполнения выглядит так:

Но что происходит, когда браузер выполняет асинхронную операцию, такую как запрос HTTP? Движок откладывает стек выполнения и начинает обрабатывать асинхронное событие или ждет его завершения?

Здесь происходит кое-что другое: поверх стека выполнения существует специальная структура данных — очередь (queue), также известная как очередь событий (event queue). Эта очередь обрабатывает асинхронные вызовы, такие как сетевые запросы, происходящие в браузере:

Движок JS обрабатывает элементы в очереди после опустения стека. Каждый раз, когда стек становится пустым, движок проверяет очередь событий, берет из нее очередной элемент и обрабатывает событие.

Важно отметить, что движок проверяет очередь, только когда стек пуст или единственным элементом в нем является глобальный контекст выполнения.

Хотя мы называем эти события асинхронными, важно понимать: события являются асинхронными по тому, как они попадают в очередь, а не по тому, когда они обрабатываются.

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

Это именно та проблема, которую решает Fiber, почти заново реализуя стек с умными возможностями — приостановка, возобновление и прерывание работы, например.

Как отмечает Andrew Clark, «Fiber — это реализация стека, специализированная для компонентов React. О волокне (fiber) можно думать как о виртуальном кадре стека (virtual stack frame). Преимущество собственной реализации стека заключается в том, что кадры стека можно хранить в памяти и выполнять как и когда угодно. Это критично для целей планирования, которых мы хотели достичь. Кроме планирования, ручное управление стеком открывает новые возможности для конкурентности (concurrency) и границ ошибок (error boundaries)». Мы поговорим об этом позже.

Простыми словами, волокно представляет собой единицу работы с собственным виртуальным стеком. В предыдущей реализации алгоритма согласования React создавал дерево объектов (элементы React), которые были иммутабельными/неизменяемыми, и обходил дерево рекурсивно.

В текущей реализации React создает дерево узлов-волокон (fiber nodes), которые могут меняться. Узел-волокно содержит состояние, пропы компонента и элемент DOM, который рендерит компонент.

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

В случае дерева волокон, React не выполняет рекурсивный обход. Вместо этого, он создает односвязный список (singly-linked list) и выполняет обход с приоритетом предка и сначала в глубину (parent-first, depth-first traversal).

❯ Что такое односвязный список узлов-волокон?

Узел-волокно представляет собой кадр стека и экземпляр компонента React. Узел содержит следующие свойства:

  • Type

  • Key

  • Child

  • Sibling

  • Return

  • Alternate

  • Output

Type

Например, <div> или <span>, хостовые компоненты (строки), классы или функции для сложных компонентов.

Key

Это тоже самое, что key, передаваемый элементу React.

Child

Представляет элемент, возвращаемый при вызове render() компонента:

const Name = (props) => {
  return (
    <div className="name">
      {props.name}
    </div>
  )
}

Child компонента <Name> является элемент <div>.

Sibling

Представляет случай, когда render() возвращает список элементов:

const Name = (props) => {
  return ([<Customdiv1 />, <Customdiv2 />])
}

<Customdiv1> и <Customdiv2> — потомки <Name>, формирующие односвязный список.

Return

Это возврат к кадру стека, что является логическим возвратом к родительскому узлу-волокну, поэтому, по сути, это предок.

pendingProps и memoizedProps

Мемоизация означает сохранение результатов выполнения функции для возможного будущего повторного использования без повторного вычисления. pendingProps представляет пропы, переданные компоненту, а memoizedProps инициализируются в конце стека выполнения, сохраняя пропы этого узла. Когда pendingProps равны memoizedProps, можно использовать предыдущий результат волокна, т.е. не выполнять лишнюю работу.

pendingWorkPriority

pendingWorkPriority — число-индикатор приоритета работы, представленной волокном. Модуль ReactPriorityLevel содержат разные уровни приоритета и их значения. За исключением NoWork — 0, более высокое число означает более низкий приоритет.

Например, следующую функцию можно использовать для проверки, что приоритет волокна, как минимум, также высок, как указанный уровень:

function matchesPriority(fiber, priority) {
  return fiber.pendingWorkPriority !== 0 &&
         fiber.pendingWorkPriority <= priority
}

Планировщик использует поле приоритета для определения следующей единицы работы (unit of work) для выполнения.

Alternate

В любой момент экземпляр компонента имеет, как минимум, два волокна, связанных с ним: текущее волокно и формирующееся (in-progress) волокно. Alternate текущего волокна — это формирующееся волокно, и наоборот. Текущее волокно представляет результат рендеринга, а формирующееся— концептуально, кадр стека, который еще не вернулся.

Output

Output — это листовые узлы приложения React. Они специфичны для среды рендеринга (например, в браузерном приложении — это div и span). В JSX они помечаются с помощью названий тегов в нижнем регистре.

Концептуально, output волокна — это значение, которое вернула функция. Каждое волокно, в конечном счете, имеет output, но он создается только в листовых узлах хостовыми компонентами. Output затем передается вверх по дереву.

В конечном счете, output передается рендереру, чтобы он применил изменения в среде рендеринга. Например, рассмотрим дерево волокон для следующего кода:

import { createRoot } from "react-dom/client";

const Parent1 = (props) => {
  return (
    <>
      <Child11 />
      <Child12 />
    </>
  )
}

const Parent2 = (props) => {
  return <Child21 />
}

const App = (props) => {
  return (
    <div>
      <Parent1 />
      <Parent2 />
    </div>
  )
}

const root = createRoot(document.getElementById("root"));

root.render(<App />);

Мы видим, что дерево волокон состоит из односвязных списков дочерних узлов, привязанных друг к другу (отношения сиблингов) и связного списка отношений «предок-потомок». Это дерево можно обойти с помощью поиска сначала в глубину (DFS).

❯ Что происходит на стадии рендеринга?

На стадии рендеринга React строит и обновляет формирующееся дерево волокон и определяет, какие изменения могут быть зафиксированы (committed).

Для понимания того, как React строит это дерево и выполняет на нем алгоритм согласования, рассмотрим юнит-тест из исходного кода React с привязанным отладчиком для отслеживания процесса; вы можете клонировать код и перейти в эту директорию.

Сначала добавляем тест Jest и привязываем отладчик. Это простой тест для рендеринга кнопки с текстом. При клике по кнопке приложение уничтожает кнопку и рендерит <div> с другим текстом, поэтому текст — это переменная состояния:

let React;
let ReactDOM;

describe('ReactUnderstanding', () => {
  beforeEach(() => {
    React = require('react');
    ReactDOM = require('react-dom');
  });

  it('works', () => {
    let instance;

    class App extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          text: "hello"
        }
      }

      handleClick = () => {
        this.props.logger('before-setState', this.state.text);
        this.setState({ text: "hi" })
        this.props.logger('after-setState', this.state.text);
      }

      render() {
        instance = this;
        this.props.logger('render', this.state.text);
        if(this.state.text === "hello") {
        return (
          <div>
            <div>
              <button onClick={this.handleClick.bind(this)}>
                {this.state.text}
              </button>
            </div>
          </div>
        )} else {
          return (
            <div>
              hello
            </div>
          )
        }
      }
    }
    const container = document.createElement('div');
    const logger = jest.fn();
    ReactDOM.render(<App logger={logger}/>, container);
    console.log("clicking");
    instance.handleClick();
    console.log("clicked");

    expect(container.innerHTML).toBe(
      '<div>hello</div>'
    )

    expect(logger.mock.calls).toEqual(
      [["render", "hello"],
      ["before-setState", "hello"],
      ["render", "hi"],
      ["after-setState", "hi"]]
    );
  })
});

При первом рендеринге React создает текущее дерево. createFiberFromTypesAndProps() - это функция, которая создает каждое волокно с помощью данных определенного элемента. При запуске теста добавляем контрольную точку на эту функцию и изучаем стек вызовов:

Как мы видим, стек вызовов ведет к вызову render(), который в конечном счете приводит к createFiberFromTypesAndProps(). Здесь есть еще несколько функций, представляющих интерес: workLoopSync(), performUnitOfWork() и beginWork().

workLoopSync()

workLoopSync() вызывается, когда React приступает к строительству дерева, начиная с узла <App> и рекурсивно перемещаясь к <div>, <div> и <button>, которые являются потомками <App>. Функция workInProgress() содержит ссылку на следующий узел-волокно, у которого есть работа для выполнения.

performUnitOfWork()

performUnitOfWork() принимает узел-волокно на вход, берет alternate узла и вызывает beginWork(). Это эквивалент начала выполнения контекстов выполнения функции в стеке выполнения.

beginWork()

Когда React строит дерево, beginWork() просто приводит к createFiberFromTypesAndProps() и создает узлы-волокна. React рекурсивно выполняет работу и, в конечном счете, performUnitOfWork() возвращает null, что свидетельствует о достижении конца дерева.

❯ instance.handleClick()

Что произойдет при выполнении instance.handleClick(), т.е. при клике по кнопке и обновлении состояния. В этом случае React обходит дерево волокон, клонирует каждый узел и проверяет, нужно ли выполнить какую-либо работу на каждом узле.

Стек вызовов в этом сценарии выглядит так:

Хотя мы не видим completeUnitOfWork() и completeWork() в первом стеке, мы видим их здесь. Как и performUnitOfWork() и beginWork(), эти две функции выполняют завершающую часть текущего выполнения, что означает возврат в стек.

Как мы видим, вместе эти четыре функции выполняют единицу работы и предоставляют контроль над текущей выполняемой работой, что отсутствовало в стековом механизме согласования.

Изображение ниже показывает, что каждый узел-волокно состоит из четырех стадий, которые требуются для завершения единицы работы:

Здесь важно отметить, что каждый узел не перемещается к completeUnitOfWork() до тех пор, пока его потомки и сиблинги не вернут completeWork().

Например, все начинается с performUnitOfWork() и beginWork() для <App/>, затем мы переходим к performUnitOfWork() и beginWork() для Parent1 и т.д. Работа на <App> завершается только после выполнения работы на всех его потомках.

На этом стадия рендеринга завершается. Новое дерево, построенное после click(), называется деревом workInProgress. По сути, это черновик дерева, ожидающий рендеринга.

❯ Что происходит на стадии фиксации?

После завершения стадии рендеринга React переходит к стадии фиксации (commit phase), где в основном происходит смена корневых указателей с текущего дерева на дерево workInProgress, т.е. текущее дерево заменяется на черновик дерева, построенный на основе обновления, запущенного click().

React также повторно использует старое дерево после перемещения указателя на дерево workInProgress. Эта оптимизация предназначена для выполнения плавного перехода (transition) от предыдущего состояния приложения к следующему, затем к следующему, и т.д.

Что насчет 16 мс времени кадра? React запускает внутренний таймер для каждой единицы работы, которая выполняется, и постоянно следит за ним. При достижении лимита, React приостанавливает текущую единицу работы и возвращает управление основному потоку, чтобы браузер выполнил рендеринг готовых элементов.

Затем, в следующем кадре, React продолжает строить дерево с того места, на котором остановился. Затем, если времени хватает, он фиксирует (commit) дерево workInProgress и завершает рендеринг.

❯ Какие основные возможности были добавлены в React Fiber, начиная с React v16?

Fiber изменил синхронный обход дерева React на систему рендеринга с планированием (scheduling). Современный React строится на этой основе, позволяя приоритизировать, прерывать работу, передавать данные потоком (streaming), распределять работу между сервером и клиентом и выполнять различные оптимизации при сборке.

Рассмотрим основные улучшения и изменения Fiber, начиная с React v16.

Конкурентный рендеринг

Самое важное обновление React Fiber с момента его появления — это конкурентный рендеринг (concurrent rendering). Это фундаментальное улучшение модели рендеринга React, основанное на возможностях React Fiber, предоставляющее прерываемый рендеринг.

Конкурентный рендеринг позволяет React готовить обновления в фоновой режиме и прерывать их при появлении более срочной работы. Другим словами, React может начать рендеринг одного обновления, прервать его для обработки более срочного обновления и затем возобновить или отменить прерванный рендеринг.

Это в корне отличается от традиционной синхронной модели рендеринга, когда React должен завершить каждый рендеринг в одной непрерываемой транзакции перед переходом к другой работе.

Хотя конкурентность была частью оригинальной версии Fiber, React 18 и 19 сделали ее возможности более доступными через такие API, как переходы (transitions), Suspense, useDeferredValue(), Activity, а также новый корневой API (createRoot()).

Suspense и селективная гидратация

Suspense — это возможность, тесно связанная с конкурентным режимом, которая первоначально была представлена с React Fiber в v16, но ее возможности были существенно расширены в обновлениях React 17 и 18.

С момента первого релиза эта возможность претерпела значительные улучшения. Изначально она была спроектирована для управления ленивой загрузкой (lazy loading) (разделение кода) компонентов с помощью React.lazy(), но ее функционал был расширен, особенно с появлением конкурентного рендеринга в v18.

Сейчас Suspense играет ключевую роль в обработке асинхронных операций, таких как получение данных, и предлагает более точечный контроль рендеринга UI в процессе выполнения этих операций. Мы можем декларативно определять, что React должен показывать, когда часть дерева еще не готова к рендерингу.

Это делается с помощью пропа fallback, который рендерит компонент или строку на время выполнения асинхронной задачи:

<Suspense fallback={<div>Loading data...</div>}>
  <SomeComponent />
</Suspense>

Другое важное улучшение — потоковая передача данных (streaming) в серверном рендеринге (SSR) и селективная гидратация (selective hydration). Вместо того, чтобы ждать, когда сервер закончит рендеринг всей страницы перед отправкой HTML, Suspense позволяет разделить страницу на независимые части. Сервер может отправить начальный скелет HTML и «стримить» оставшийся контент по мере его готовности.

С селективной гидратацией Suspense делит дерево на небольшие единицы гидратации. Затем React гидратирует эти части независимо, что позволяет сделать одни контролы интерактивными в то время, как другие части ждут выполнения кода или поступления данных.

В React 19 механизм Suspense продолжает эволюционировать, особенно в том, что касается планирования задач для сиблингов. Когда компонент приостанавливается (suspends), React может раньше зафиксировать (commit) ближайший резерв (fallback), а затем запланировать еще один рендеринг для предварительной подготовки ленивых (lazy) ресурсов в оставшемся дереве сиблингов. Это позволяет резерву появиться раньше, не препятствуя началу загрузки других ресурсов.

Suspense — это сложная система. Если вы хотите узнать о ней больше, ознакомьтесь с нашей подробной статьей о ней.

Переходы

Переходы (transitions) — один из основных API конкурентного рендеринга. Он позволяет разделять срочные и несрочные обновления состояния.

Срочные обновления включают такие задачи, как обработка ввода пользователя или проигрывание анимации, когда ожидается немедленная обратная связь. Несрочные обновления, такие как рендеринг списка результатов поиска, менее приоритетны, поэтому допустима небольшая задержка рендеринга.

Хук useTransition() позволяет помечать обновления состояния как несрочные. По умолчанию, все обновления считаются срочными.

useTransition() обрабатывает переход между состояниями UI неблокирующим способом. Он возвращает массив с двумя значениями:

  • isPending — логический индикатор выполнения перехода

  • startTransition() — функция для запуска перехода

Например, рассмотрим страницу с вкладками/табами, где рендеринг вкладки занимает много времени:

import { useState, useTransition } from "react";
import About from "./About";
import Posts from "./Posts";

export default function TabContainer() {
  const [activeTab, setActiveTab] = useState("about");
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab) {
    startTransition(() => {
      setActiveTab(nextTab);
    });
  }
  return (
    <>
      <button onClick={() => selectTab("about")}>About</button>
      <button onClick={() => selectTab("posts")}>Posts</button>
      {isPending && <p>Loading tab...</p>}
      {activeTab === "about" ? <About /> : <Posts />}
    </>
  );
}

Обновление состояния внутри startTransition() считается несрочным. Если пользователь нажмет на другую кнопку во время рендеринга вкладки Posts, React может прервать этот рендеринг и переключиться на обработку пользовательского взаимодействия.

Переходы не откладывают вызов функции, переданной в startTransition(). React вызывает функцию сразу. Планируется только обновление состояния.

Это означает, что помещение дорогой с точки зрения производительности операции в startTransition() не приведет к ее выполнению в фоновом режиме:

startTransition(() => {
  // Вычисление выполняется сразу
  const filteredItems = items.filter(expensiveFilter);
  setFilteredItems(filteredItems);
});

Вызов filter() заблокирует основной поток. Еще раз: startTransition() меняет только приоритет обновлений состояния.

Асинхронные переходы и операции

В React 19 функции, передаваемые в startTransition(), называются операциями (actions). Операция может выполнять асинхронную работу, такую как запрос (request), а isPending представляет индикатор обрабатываемого взаимодействия.

function SettingsForm() {
  const [settings, setSettings] = useState(initialSettings);
  const [isPending, startTransition] = useTransition();

  function saveSettings(nextSettings) {
    startTransition(async () => {
      const savedSettings = await updateSettings(nextSettings);
      startTransition(() => {
        setSettings(savedSettings);
      });
    });
  }

  return (
    <button
      disabled={isPending}
      onClick={() => saveSettings({ theme: "dark" })}
    >
      {isPending ? "Saving..." : "Save settings"}
    </button>
  );
}

В настоящее время обновления после await должны запускаться в другом startTransition(), чтобы считаться переходами. Это известное ограничение, связанное с тем, что React теряет контекст при переходе через асинхронную границу.

Асинхронные переходы соединяют конкурентный рендеринг с новыми возможностями React, такими как Action, <form action={action}>, useActionState() и useOptimistic(). Они позволяют React координировать ожидающий (pending) UI, асинхронную работу и финальный рендеринг в одной операции.

useDeferredValue()

Хук useDeferredValue() позволяет обновлять одну часть UI после другой. Это полезно, когда изменения значения являются срочными, но компонент, который зависит от него, рендерится медленно.

Например, ввод пользователя должен отображаться немедленно, а большой список результатов поиска может обновляться с меньшим приоритетом:

import { Suspense, useDeferredValue, useState } from "react";

export default function SearchPage() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);

  const isStale = query !== deferredQuery;

  return (
    <>
      <input
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Search"
      />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <Suspense fallback={<p>Loading results...</p>}>
          <SearchResults query={deferredQuery} />
        </Suspense>
      </div>
    </>
  );
}

В этом примере, когда query обновляется, React сразу рендерит инпут с последним значением, а deferredQuery содержит предыдущее значение.

Затем React выполняет другое обновление в фоновом режиме, используя последнее значение. Фоновый рендеринг прерываем. Если пользователь еще что-то введет перед его завершением, React может отменить его и перезапустить с новым значением.

В отличие от традиционной задержки (debounce), useDeferredValue() не ждет фиксированное время. React пытается выполнить отложенный рендеринг максимально быстро.

Основное отличие между useTransition() и useDeferredValue() состоит в том, когда они применяются:

  • используйте useTransition(), когда управляете состоянием и можете обернуть его сеттер в startTransition()

  • используйте useDeferredValue(), когда получаете значение через проп, хук или другим способом и хотите, чтобы медленная часть сохранялась между быстро меняющимися значениями

Activity

React 19.2 представил компонент <Activity> — еще одну возможность на основе конкурентного рендеринга. Он позволяет скрывать и восстанавливать часть UI без потери ее внутреннего состояния.

import { Activity, useState } from "react";

export default function Tabs() {
  const [activeTab, setActiveTab] = useState("home");

  return (
    <>
      <button onClick={() => setActiveTab("home")}>Home</button>
      <button onClick={() => setActiveTab("settings")}>Settings</button>
      <Activity mode={activeTab === "home" ? "visible" : "hidden"}>
        <Home />
      </Activity>
      <Activity mode={activeTab === "settings" ? "visible" : "hidden"}>
        <Settings />
      </Activity>
    </>
  );
}

Когда Activity видим, React рендерит его как обычно и монтирует его эффекты. Когда он скрывается, React скрывает контент его DOM, очищает его эффекты и сохраняет его компонент и состояние DOM. Обновления скрытого контента по-прежнему рендерятся, но планируются с более низким приоритетом, чем видимая работа. Когда Activity снова становится видимым, React восстанавливает сохраненное состояние и воссоздает его эффекты.

Activity четко показывает, как Fiber может сохранять завершенную работу, понижать ее приоритет в скрытом состоянии и повторно использовать ее, когда она снова становится релевантной.

Вместе, переходы, Suspense, селективная гидратация, потоковый серверный рендеринг, Activity и отложенные значения отражают разные аспекты модели конкурентного рендеринга React.

❯ Как useSyncExternalStore() защищает внешнее состояние в конкурентном React?

Конкурентный рендеринг представляет серьезную проблему для состояния, хранящегося снаружи React. Внешнее хранилище может измениться в то время, когда React приостановил рендеринг дерева компонентов. Если разные компоненты читают разные версии хранилища, React может отрендерить несогласованный UI.

React 18 представил useSyncExternalStore() для того, чтобы React мог подписываться на внешние хранилища и читать согласованные снимки (snapshots) состояния:

import { useSyncExternalStore } from "react";

function OnlineStatus() {
  const isOnline = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot,
  );

  return <p>{isOnline ? "Online" : "Offline"}</p>;
}

Здесь первый аргумент — подписка на изменения внешнего источника, второй возвращает текущее значение, а опциональный третий предоставляет значение, используемое при серверном рендеринге.

useSyncExternalStore() особенно полезен для библиотек управления состоянием и браузерных API, чьи значения могут измениться за пределами системы обновлений React.

❯ Автоматическая группировка

Автоматическая группировка (batching) помогает уменьшить количество повторных рендерингов, происходящих при изменении состояния, позволяя React группировать несколько обновлений состояния в один повторный рендеринг.

Например, при вызове нескольких setState() в одном цикле рендеринга, React автоматически группирует их в одно обновление:

function MyComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // Несколько обновлений состояния в одном обработчике событий
    setCount(count + 1);
    setCount(count + 2);
  };

  console.log("Rendering")

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

В этом примере React автоматически группирует два обновления состояния в одно. Это означает, что происходит только один повторный рендеринг и «Rendering» выводился в консоль лишь раз.

До React v18 группировались только обновления внутри обработчиков событий. Обновления за пределами жизненного цикла React (например, внутри setTimeout() или промиса) не группировались. В React 18+ группируются обновления, происходящие во всех контекстах.

❯ Оптимизация кода React Compiler во время сборки

React Compiler добавляет мемоизацию во время сборки для уменьшения количества работы, осуществляемой Fiber во время выполнения (runtime).

Ранее мы видели, что узел-волокно хранит pendingProps и memoizedProps в процессе согласования для определения работы для Fiber.

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

Мемоизация происходит во время выполнения после получения React обновления и начала процесса согласования. Fiber затем решает, какие части дерева можно использовать повторно.

Такие инструменты, как React.memo, useMemo() и useCallback() могут помочь React мемоизировать компоненты, когда это возможно.

import { memo, useCallback, useMemo } from "react";

const ProductList = memo(function ProductList({ products, onSelect }) {
  const availableProducts = useMemo(() => {
    return products.filter((product) => product.inStock);
  }, [products]);

  const handleSelect = useCallback(
    (productId) => {
      onSelect(productId);
    },
    [onSelect],
  );

  return (
    <ul>
      {availableProducts.map((product) => (
        <Product key={product.id} product={product} onSelect={handleSelect} />
      ))}
    </ul>
  );
});

Каждый хук обрабатывает разные части мемоизации:

  • React.memo — позволяет React пропустить рендеринг ProductList, если его пропы не изменились

  • useMemo() — кэширует результат фильтрации

  • useCallback() — сохраняет ссылку на функцию между рендерингами

Хотя эти API помогают уменьшить количество работы для Fiber, разработчикам нужно самим решать, что и когда мемоизировать, и следить за актуальностью массива зависимостей.

React Compiler предназначен для уменьшения ручной работы и добавляет еще один уровень оптимизации. Вместо того, чтобы идентифицировать лишнюю работу во время выполнения, компилятор анализирует компоненты и хуки во время сборки и автоматически добавляет мемоизацию, где это возможно и безопасно.

Это означает, что мы можем переписать компонент следующим образом:

function ProductList({ products, onSelect }) {
  const availableProducts = products.filter((product) => product.inStock);

  function handleSelect(productId) {
    onSelect(productId);
  }

  return (
    <ul>
      {availableProducts.map((product) => (
        <Product key={product.id} product={product} onSelect={handleSelect} />
      ))}
    </ul>
  );
}

В процессе сборки компилятор анализирует, какие значения зависят от пропов, состояния и других значений в компоненте. Затем он может сгенерировать проверки кэша, которые переиспользуют availableProducts, handleSelect, элементы JSX и результат рендеринга дочерних компонентов, при условии, что их входные данные остаются неизменными.

Это не означает, что компилятор сохраняет финальный DOM или пропускает согласование. Скомпилированный компонент по-прежнему запускается внутри обычной системы рендеринга React. Fiber по-прежнему создает и согласовывает формирующееся дерево и решает, что должно быть зафиксировано (committed).

React Fiber и React Compiler — разные системы. В таблица ниже представлены решаемые ими задачи и ответы на популярные вопросы об их совместной работе.

❯ React Fiber и React Compiler: основные отличия и часто задаваемые вопросы

Вопрос

React Fiber

React Compiler

Когда работает система?

Работает во время выполнения, когда React рендерит и сравнивает деревья компонентов

Работает, в основном, во время сборки, анализируя и трансформируя код приложения переда запуском

На какую информацию полагается система?

Использует данные времени выполнения, такие как пропы Fiber, состояние, контекст, приоритеты обновления и ранее выполненная работа

Использует статический анализ компонентов, хуков, значений, функций, JSX и их зависимостей

Какая степень вовлеченности разработчика требуется для оптимизации?

React выполняет согласование автоматически, но разработчикам часто нужно вручную добавлять React.memo, useMemo() и useCallback() для оптимизации производительности

Компилятор выполняет оптимизацию путем добавления большей части мемоизации автоматически

Как система связана с конкурентным рендерингом?

Fiber действует как планировщик, который может приостанавливать, перезапускать, приоритизировать и прерывать рендеринг

Compiler уменьшает общий объем работы, которую должен выполнить конкурентный рендерер

Как система влияет на DOM?

В конечном итоге, Fiber применяет необходимые изменения DOM через рендерер

Compiler не обновляет DOM напрямую

React Compiler заменяет React Fiber?

Нет, Fiber остается основным механизмом согласования времени выполнения React

Скомпилированные компоненты по-прежнему рендерятся с помощью Fiber во время выполнения

❯ Заключение

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


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале