Головна / Статті / Усередині React Fiber: одиниці роботи, Render проти Commit та канали пріоритету

Усередині React Fiber: одиниці роботи, Render проти Commit та канали пріоритету

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

3495 слів

„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, приєднуються refs та виконуються ефекти макетування. Корисний спосіб розділити ці два етапи:

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 у робочий процес. Якщо у вас є обчислення, які вимагають багато ресурсів CPU та не пов’язані з відображенням, такі як парсинг чи обробка чисел, робочий процес все одно залишається правильним інструментом; стаття про спеціалізовані, спільні та сервісні робочі процеси детально розглядає ці варіанти.

Що це означає для вашого коду

function App() {
    return <Dashboard />;
}

і ви запускаєте оновлення:

setState(newValue);

React у фоновому режимі перетворює це на операції, які виконуються через фібри. Ваш код знаходиться на початку потоку обробки даних, про нижні рівні якого вам рідко доводиться турбуватися:

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.
  • Fiber — це об’єкт, який представляє одиницю роботи з відображення, пов’язану у структуру дерева, яку React може просуватися та призупиняти.
  • На етапі відображення обчислюються зміни, і його можна перервати; на етапі підтвердження ці зміни застосовуються одним безперервним кроком.
  • Lanes та планувальник використовують структуру Fiber, щоб надати пріоритет терміновим оновленням перед відкладеними.
  • Fiber — це не Віртуальний DOM і не багатопотоковість; це співпраця у плануванні завдань на основному потоці.
  • Одночасне відображення та переходи будуються на цій основі, але вони керують ресурсоємними операціями, а не усувають їх.
  • Пов’язана література