Внутри React Fiber: единицы работы, процессы отрисовки и сохранения изменений, а также каналы приоритетов
Узнайте, что на самом деле такое React Fiber, почему старый механизм согласования блокировал основной поток, и как единицы работы, две фазы и каналы обеспечивают одновременную отрисовку.
«Fiber» — это один из терминов React, который упоминается гораздо чаще, чем объясняется. Разработчики слышат о нём как об новом двигателе, заменителе Virtual DOM или связанном с Hooks, но ни одно из этих описаний не совсем точно. В этом руководстве концепция излагается с нуля: сначала рассматриваются проблемы React до версии 16, затем что такое Fiber, как рендеринг делится на две фазы и как функционируют планирование и приоритеты. К концу вы сможете точно объяснить, что такое Fiber, распознавать распространённые заблуждения и понимать, почему такие API, как startTransition, от него зависят.
Fiber в одном предложении
React Fiber — это внутренняя архитектура согласования, поставляемая вместе с React 16. Её задача — сделать процесс отрисовки управляемым. Вместо того чтобы рассматривать обновление как единое, неделимое действие, React моделирует его как множество небольших единиц, которые можно обрабатывать по одной и расставлять в порядке значимости.
Эту смену легче понять, если рассмотреть процесс параллельно. До появления Fiber обновление проходило прямой линией от начала до конца:
Before Fiber:
Update
↓
Render entire tree
↓
Commit changes
↓
Done
С появлением Fiber посередине появляется этап, на котором работа разбивается на части, и эти части можно упорядочить, приостановить и возобновить до того, как что-либо появится на экране:
After Fiber:
Update
↓
Break work into units
↓
Process units
↓
Prioritize / pause / resume when appropriate
↓
Commit changes
Этот дополнительный этап лежит в основе практически всех современных функций отрисовки в React. Чтобы понять, почему он оправдал переписывание кода, взгляните на то, что было раньше.
Согласователь стека и его слабое место
Версии React до 16 использовали то, что обычно называют Stack Reconciler. Общая схема работы была уже знакомой: изменение состояния приводило к отрисовке, отрисовка сравнивалась с предыдущим результатом, и затем обновлялся DOM.
State Change
↓
Render
↓
Reconciliation
↓
DOM Updates
Проблема заключалась в том, что процесс согласования выполнялся синхронно. Название происходит от того, что механизм согласования просматривал структуру данных с помощью обычных рекурсивных вызовов функций, поэтому ход выполнения операций находился непосредственно в стеке вызовов JavaScript. Как только React начинал обрабатывать обновление, у него не было способа остановиться на полпути, поскольку остановка означала бы разворачивание этого стека и потерю информации о его состоянии. Представьте себе приложение умеренного размера:
App
│
├── Header
├── Sidebar
├── Dashboard
│ ├── Chart
│ ├── Table
│ └── Statistics
├── Notifications
└── Footer
Если обновление затрагивает большую часть этой структуры, React проходит по затронутым компонентам в одном непрерывном цикле, от первого до последнего, так и не возвращая управление обратно:
Start rendering
↓
Component A
↓
Component B
↓
Component C
↓
Component D
↓
Component E
↓
...
↓
Finish
Результат работы был нормальным; проблема заключалась во времени. React не мог прервать свою работу и возобновить её позже, поэтому всё остальное вынуждено было ждать.
Почему длительная обработка влияет на пользователя
У JavaScript нет отдельной основной нити. Браузер использует ту же нить для выполнения обработчиков событий, расчёта макета, отрисовки пикселей и генерации кадров:
Browser Main Thread
│
├── JavaScript
├── Event Handling
├── Layout
├── Paint
└── Rendering
Когда скрипт долго занимает нить, все остальные задачи выстраиваются в очередь после него. С точки зрения пользователя последовательность событий выглядит следующим образом:
Click
↓
React starts large update
↓
Main thread remains busy
↓
Browser can't respond quickly
↓
UI feels slow
Нажатие кнопки фиксируется с задержкой, набор символов появляется спустя заметную паузу, анимация тормозит. В небольшом приложении процесс отрисовки длится достаточно коротко, чтобы никто этого не заметил. Однако по мере роста приложения и увеличения количества взаимодействий фреймворку необходимо контролировать когда выполняются операции отрисовки и сколько из них происходит одновременно. Именно этот пробел и предназначен был к устранению Fiber.
Основная идея: работа в виде планируемых единиц
Концепция Fiber можно описать одной фразой: отрисовка разбивается на управляемые единицы работы, которые React может планировать и приоритизировать.
По старой модели команда для механизма синхронизации фактически представляла собой единственную инструкцию:
"Render this entire tree."
В Fiber React может анализировать отдельные элементы и решать, какие из них требуют внимания в первую очередь:
"Here is one piece of work."
"Here is another piece."
"Which work should I process first?"
Вместо глубоких рекурсивных вызовов с использованием отдельных элементов React может остановиться после любого из них, проверить наличие более важных задач и продолжить позже.
Что такое фибра
Фибра — это обычный объект JavaScript, представляющий собой одну единицу работы во внутренней структуре React. На практике примерно одна фибра соответствует каждому компоненту или элементу, участвующему в процессе согласования данных. Возьмём эту небольшую структуру компонентов:
App
│
├── Header
├── Sidebar
└── Content
Концептуально React использует параллельную структуру, состоящую из фибр:
Fiber Tree
App Fiber
│
├── Header Fiber
├── Sidebar Fiber
└── Content Fiber
Реальные объекты Fiber содержат гораздо больше, чем просто имя. Они хранят информацию о связях с родительскими, дочерними и соседними узлами, типе и идентификаторе компонента, ожидающихся параметрах и состоянии, а также об эффектах, которые необходимо выполнить после завершения обработки. Именно эти явные связи позволяют React просматривать дерево с помощью цикла вместо рекурсии, что в свою очередь обеспечивает возможность остановки и продолжения работы. Однако для всей архитектуры достаточно запомнить одну формулу: Fiber — это единица работы React по отрисовке и согласованию состояния.
Fiber не является заменой Virtual DOM
Часто утверждается, что Fiber «заменил Virtual DOM». Это не так. Оба инструмента решают разные задачи.
Virtual DOM — это описание того, как должен выглядеть интерфейс, с которым React сравнивает текущее состояние, чтобы определить, что необходимо изменить:
Virtual DOM
↓
"What should the UI look like?"
Fiber — это механизм, который использует React для организации и выполнения работ, необходимых для достижения поставленной цели:
Fiber
↓
"How should React organize and process the work required to get there?"
Они работают вместе, но один представляет собой интерфейс пользователя, а другой — способ обработки задач. Чтобы узнать больше о сравнении этих элементов, прочитайте как механизм сравнения Virtual DOM в React определяет, что нужно обновить.
Дерево fiber во время обновления
React сохраняет свое дерево fiber на протяжении всего срока жизни приложения. Более подробный пример показывает, как поддеревья встраиваются внутрь него:
App
│
┌─────────┼─────────┐
↓ ↓ ↓
Header Sidebar Content
│
┌──────┴──────┐
↓ ↓
Chart Table
Когда меняются props или состояние, React использует эту структуру для поиска тех ветвей, которые требуют обработки, и игнорирует остальные.
Создание возможности прерывания отрисовки
Самым важным последствием такой архитектуры является возможность приостановки процесса отрисовки. Рассмотрим обновление, которое делится на несколько этапов выполнения:
Large Update
↓
Work 1
↓
Work 2
↓
Work 3
↓
Work 4
При использовании синхронного механизма согласования все четыре этапа должны были выполняться подряд. С помощью Fiber React может обработать несколько из них, передать управление браузеру для реакции на ввод или отрисовки кадра, а затем продолжить работу:
Work 1
↓
Work 2
↓
Pause
↓
Browser gets an opportunity to handle other work
↓
Resume
↓
Work 3
↓
Work 4
Это само по себе не является оптимизацией скорости; общий объем работы остается прежним. Просто эта работа больше не монополизирует поток, что даёт React возможность запланировать её выполнение.
Два этапа: определение изменений и их применение
Современный React не рассматривает обновление как одноразовый процесс от отрисовки до изменения DOM:
Render → DOM
Вместо этого каждое обновление проходит через два отдельных этапа:
React Update
│
↓
Render Phase
│
↓
Commit Phase
│
↓
DOM
На этапе отрисовки вычисляется следующий интерфейс
На этапе отрисовки React отвечает на вопрос о том, каким должен быть интерфейс сейчас. Конкретно он:
- запускает функции компонентов и обрабатывает ожидаемые обновления
- создаёт или согласовывает дерево фиберов
- определяет, что отличается от текущего дерева
- собирает список изменений, которые необходимо применить
В виде диаграммы сначала вводятся предыдущее дерево и новые обновления, а затем выводится описание необходимых действий:
Previous Tree
+
New Updates
↓
Reconciliation
↓
New Work
Поскольку на этом этапе ничего не влияет на DOM, его можно прервать в современном React. React может вычислить большую часть следующей структуры в фоновом режиме, и никто не видит промежуточного состояния. Это также объясняет, почему React ожидает, что процесс отрисовки будет свободен от побочных эффектов: при одновременной отрисовке компонент может быть отрисован несколько раз до того, как его результат будет сохранён, а в режиме Strict Mode логика отрисовки намеренно вызывается дважды, чтобы выявить код, который нарушает это предположение.
Этап сохранения применяет результат
Как только изменения становятся известны, React сохраняет их:
Render Phase
↓
Changes determined
↓
Commit Phase
↓
DOM updated
Именно на этапе сохранения происходят реальные изменения: вставляются, обновляются или удаляются узлы DOM, привязываются ссылки, а также выполняются эффекты макетирования. Удобный способ разделить эти два этапа:
Render Phase
"Let's figure out what needs to change."
Commit Phase
"Now apply those changes."
Почему этап сохранения нельзя приостановить
Если паузы настолько полезны, почему бы не делать их везде? Потому что интерфейс должен оставаться последовательным. Представьте, что React прервет работу над записью в DOM на полпути:
Update A
↓
DOM partially changed
↓
Pause
↓
Update B
Пользователь увидит смешение старого и нового интерфейсов, которые никогда логически не существовали вместе. Поэтому React разделяет задачи: процесс обработки изменений может быть прерван, возобновлен или отклонён, но коммит применяет готовый результат за один раз. Это разделение критически важно для понимания современного React.
Планирование: не все обновления одинаковы
Разделив работу на отдельные блоки, React может определить, какие из них наиболее важны. Некоторые обновления явно более срочны, чем другие. В ответ на это:
User clicks a button
для пользователя в тот момент важнее, чем это:
Rendering a large list somewhere else
Точно так же и это:
Typing in an input
Необходимо, чтобы все происходило мгновенно. Замедление отображения символов по сравнению с вводом на клавиатуре не должно возникать из-за тяжелых обновлений в других частях страницы. Fiber обеспечивает структуру, которая позволяет React анализировать такие различия и правильно распоряжаться задачами.
Линии обработки: как React определяет приоритет
Внутри себя современные теги React используют линии обработки, которые кодируют их приоритет. Код приложения почти никогда не работает напрямую с линиями обработки; React использует их для определения того, какие задержанные обновления необходимо обработать в процессе отрисовки, а какие могут подождать. Упрощенное представление:
Updates
│
┌───────────┼───────────┐
↓ ↓ ↓
Urgent Normal Deferred
│ │ │
↓ ↓ ↓
Process Process Process
sooner normally later
Полезно разделять три связанных термина, когда они встречаются вместе:
Fiber
↓
Represents work
Scheduler
↓
Helps coordinate when work should happen
Lanes
↓
Represent priority of updates
Fiber описывает саму работу, планировщик определяет момент её выполнения, а каналы указывают на степень срочности каждого обновления. Конкретная реализация этих элементов менялась между версиями React, поэтому рассматривайте это как концептуальную модель, а не описание текущего исходного кода.
Старое против нового, шаг за шагом
До появления Fiber процесс синхронизации был основан на стеке, и как только он начинался, он просто продолжался без остановки:
Update
↓
Reconcile
↓
Continue
↓
Continue
↓
Continue
↓
Finish
Не было возможности прервать выполнение работы или отдать приоритет одному обновлению перед другим.
Архитектура Fiber включает решения по планированию прямо в процесс обработки данных:
Update
↓
Create / schedule work
↓
Process Fiber units
↓
Prioritize
↓
Pause / resume / restart when appropriate
↓
Complete render
↓
Commit
Если разместить оба варианта рядом, разница становится очевидной:
BEFORE FIBER:
Component Tree
↓
Synchronous Reconciliation
↓
Finish Everything
↓
Commit
AFTER FIBER:
Component Tree
↓
Fiber Tree
↓
Units of Work
↓
Prioritize / Schedule
↓
Render
↓
Commit
Целью не было ускорение React, а получение контроля над тем, как и когда происходит рендеринг.
Распространённые заблуждения
Fiber не добавляет потоки
Fiber не разделяет React на несколько потоков:
React
├── Thread 1
├── Thread 2
└── Thread 3
JavaScript-код React по-прежнему выполняется на главном потоке браузера. Fiber представляет собой форму совместного планирования: React добровольно уступает контроль между этапами обработки задач, чтобы другие операции могли выполниться. Это принципиально отличается от Web Workers, которые действительно выполняют код на отдельном потоке.
Конкурентность не означает одновременность
Fiber делает возможной конкурентную отрисовку, но термин «конкурентность» легко вводит в заблуждение. Он не означает, что React отрисовывает всё в один и тот же момент. Это означает, что React может подготовить обновление без блокировки приложения на весь период длительной отрисовки. Типичная последовательность:
Low-priority update
↓
React starts rendering
↓
Higher-priority update arrives
↓
React can prioritize the important work
↓
Continue / restart lower-priority work
Рендеринг с низким приоритетом можно отложить, когда появляется более срочная задача, а затем продолжить его или начать заново. Именно этот механизм обеспечивает более плавную работу многих функций в современном React. Как это реализуется в рамках фреймворка, описано в статье о частичном предварительном рендеринге и одновременном рендеринге, где рассматривается подход Next.js.
Fiber — это не «один компонент за раз»
Также легко представить, что Fiber рендерит сначала один компонент, затем следующий и так далее:
Component 1
Component 2
Component 3
Это слишком упрощённо. Процессы просмотра, группировки и планирования в React гораздо сложнее, чем у простого списка; Fiber предлагает модель работы с гораздо более тонкой степенью детализации по сравнению со старым рекурсивным подходом.
Переходы на практике
Переходы — это те моменты, когда механизм планирования Fiber становится видимым в коде приложения. Возьмем поле поиска, в которое пользователь только что ввёл текст:
User types: "rea"
Этот нажатие клавиши запускает два разных вида действий:
1. Update the input immediately
2. Update a huge search result list
Обновление введённого текста — это то, что наблюдает пользователь; пересоздание длинного списка результатов может занять много ресурсов. React позволяет отметить второй вид действий как незначимый, обернув обновление состояния в функцию startTransition:
startTransition(() => {
setSearchResults(results);
});
Введённые символы обрабатываются как срочное обновление, чтобы поле оставалось отзывчивым:
User Input
↓
Urgent Update
↓
Keep UI responsive
Список результатов обрабатывается как переход, который React может отрисовать с более низким приоритетом и прервать, если пользователь продолжит вводить текст:
Search Results
↓
Transition
↓
Can be handled with lower priority
Два практических замечания. Во-первых, переходы не делают затратную обработку дешевлей; они лишь предотвращают её влияние на срочные обновления, поэтому медленный список всё равно может извлекать пользу из мемоизации или виртуализации. Во-вторых, собственное состояние входных данных должно оставаться вне процесса перехода, иначе само ввод данных становится откладываемым, что делает поле медленным в работе. Всё это планирование было бы невозможно без фазы отрисовки с возможностью прерывания, предоставляемой Fiber.
Почему React нуждался в новой основе
До появления Fiber у React уже было большинство того, что с ним ассоциируют:
- Виртуальный DOM
- Сопоставление состояний
- Модель компонентов
- Эффективные обновления DOM
Переработка была направлена на будущее, а не на исправление уже существующих проблем. Приложения развивались одновременно в нескольких направлениях:
Larger
+
More interactive
+
More data-driven
+
More complex
Такой рост означал, что React должен был учитывать не только то, что изменилось. Ему также нужно было отвечать на вопросы о том, когда следует выполнить определенную операцию, насколько важно обновление, можно ли приостановить работу, должно ли другое обновление пропустить очередь, и как избежать блокировки взаимодействий, которые наиболее важны для пользователей. Fiber — это архитектура, позволяющая находить ответы на эти вопросы.
Сценарий панели управления
Рассмотрим аналитическую панель управления с несколькими ресурсоемкими разделами:
Dashboard
│
├── Navigation
├── Filters
├── Revenue Chart
├── User Chart
├── Large Data Table
└── Notifications
Когда пользователь меняет фильтр, примитивная реализация может перерисовывать графики и большую таблицу за один проход, в результате чего сам контроллер фильтра будет реагировать медленно. В архитектуре Fiber та же взаимодействие проходит через приоритизированные операции:
User changes filter
↓
Update begins
↓
React performs reconciliation
↓
Work is represented through Fiber
↓
Updates can be prioritized
↓
Rendering completes
↓
Commit changes
Диаграммы и таблицы всё ещё нужно пересчитывать. Улучшается же отзывчивость благодаря тому, что React берёт на себя эту работу.
Fiber по сравнению с смежными концепциями
Fiber и виртуальный DOM
Виртуальный DOM — это представление интерфейса, которое используется React для определения того, что нужно изменить:
UI State
↓
Virtual Representation
Fiber — это внутренняя архитектура и структура данных, с помощью которых React представляет и обрабатывает задачи отрисовки:
Component / Element
↓
Fiber
↓
Reconciliation + Scheduling
Вместе они образуют систему отрисовки React:
Virtual DOM
+
Fiber Architecture
↓
React Rendering System
Связаны, но не взаимозаменяемы.
Fiber и Web Workers
Они решают совершенно разные проблемы. Fiber касается:
Rendering
Reconciliation
Scheduling
Prioritization
В то время как Web Worker предназначен для:
Running JavaScript
outside the main UI execution context
Его структура выглядит так: тяжёлые вычисления переносятся с потока, управляющего интерфейсом:
Main Thread
│
├── UI
├── React
│
↓
Web Worker
│
↓
Heavy computation
Fiber никогда не перемещает процесс отрисовки в React в рабочий поток. Если у вас есть задачи, требующие мощности процессора, которые не связаны с отрисовкой, такие как парсинг или вычисления, рабочий поток по-прежнему является подходящим инструментом; статья о специализированных, общих и сервисных рабочих потоках рассматривает эти варианты.
Что это означает для вашего кода
В повседневной работе вы никогда не взаимодействуете напрямую с fiber. Вы пишете компоненты:
function App() {
return <Dashboard />;
}
и запускаете обновления:
setState(newValue);
React скрытно преобразует их в задачи для fiber. Ваш код находится на вершине цепочки обработки, о нижних уровнях которой вам редко нужно беспокоиться:
Your React Code
↓
React APIs
↓
Fiber Architecture
↓
Reconciliation
↓
Scheduling / Prioritization
↓
Commit
↓
DOM
Вы никогда не создаете узлы Fiber вручную. Вам нужна следующая концепция: сохраняйте логику отрисовки в чистом виде, помещайте побочные эффекты в специальные компоненты или обработчики, а для дорогостоящих и незакритичных обновлений используйте переходы.
Краткое резюме
Старый механизм согласования следовал простому правилу:
"Start rendering → keep going → finish."
Fiber следует другому правилу:
"Break rendering into work → decide how to schedule it
→ complete the render → commit the result."
Именно в этом различии заключается суть архитектурных изменений.
Заключение
Механизм согласования на основе стека хорошо служил React в течение многих лет. Fiber решает проблемы приложений, которые выросли за рамки его возможностей. Эволюция происходит от ограниченного контроля:
Old React
↓
Synchronous Stack Reconciler
↓
Limited control over rendering work
к модели, в которой работа является четко определенной, планируемой и имеющей приоритеты:
React Fiber
↓
Fiber Tree + Units of Work
↓
More flexible reconciliation
↓
Scheduling + Prioritization
↓
Concurrent Rendering Capabilities
Когда кто-то спрашивает, что такое React Fiber, формулировка «новый движок отрисовки» недооценивает его значение. Более точный ответ заключается в том, что Fiber — это внутренняя архитектура согласования данных в React, которая моделирует процесс отрисовки как наборы задач, позволяя React планировать их выполнение, устанавливать приоритеты, прерывать и возобновлять в нужный момент.
Основные выводы
- Fiber появился в React 16 и заменил рекурсивный, синхронный механизм Stack Reconciler.
- Файбер — это объект, представляющий одну единицу работы по отрисовке, встроенную в дерево, которое React может просматривать и приостанавливать.
- На этапе отрисовки вычисляются изменения, и его можно прервать; на этапе подтверждения изменения применяются без перерывов.
- Механизмы Lane и планировщик используют структуру Fiber, чтобы придавать приоритет срочным обновлениям перед отложенными.
Связанные материалы
- Планировщик React и цикл событий: кто на самом деле решает, когда выполняться задачи — Узнайте, как совместный планировщик React работает внутри JavaScript-цикла событий, почему переходы могут быть приостановлены и почему ни один планировщик не может спасти заблокированный поток.
- Понимание React Lanes: как битовые маски кодируют приоритет обновлений — Узнайте, как React использует битовые маски для кодирования нескольких приоритетов обновлений в одно целое число, и почему для планирования используются битовые операции вместо простых логических флагов.