Опубліковано
29 хв читання

Як React рендерить інтерфейс і що насправді робить Fiber

Автори
  • Learna
    Ім’я
    Learna

Більшість статей про React Fiber написано між 2016 і 2020 роками. Відтоді React істотно змінився: описану там систему пріоритетів замінили, старого списку ефектів уже немає, а популярне пояснення про бюджет кожного кадру ніколи точно не відображало його роботу.

Це стаття, якої мені бракувало на початку вивчення теми: від основ, простими словами, з перевіркою тверджень за вихідним кодом React 19.3.0. Дату й версію тут збережено з англійського оригіналу: 9 вересня 2026 року.

Як читати. Частини 1 і 2 — основа. Прочитайте їх навіть якщо вже знайомі з Fiber: далі використовуються саме ці поняття. Частини 3 й 4 корисні всім. Частина 5 — заглиблення в механізми пріоритетів, які найбільше змінилися порівняно зі старими статтями. Частина 7 підсумовує, як усе це впливає на щоденний код.

Про версії

Текст описує React 19.3.0. Для змін зазначено версії, експериментальні можливості позначено окремо. За задумом оригінальної статті, неперевірені твердження не включалися.

Частина 1. Що означає рендеринг

Render, commit і paint

Ці три поняття постійно плутають, і саме звідси починається більшість непорозумінь щодо швидкодії React.

Render — React викликає функцію компонента, і вона повертає елементи. Екран на цьому етапі ще не обов’язково змінюється.

Commit — React застосовує потрібні зміни до DOM. Оновлюються ті частини, що справді відрізняються.

Paint — браузер малює пікселі. React сам їх не малює.

Рендеринг часто взагалі не завершується змінами DOM. Функцію викликали, порівняли результат і не знайшли роботи для DOM. Лічильник викликів компонента сам собою не доводить проблему швидкодії. Водночас дорогим може бути і сам рендеринг: його вартість залежить від вашого коду, а не лише від commit.

ReactбраузертригерrendercommitpaintsetState()виклик кодузміни DOMпікселіможливі паузибез перериваньефект може запустити наступний render

React виконує render і commit; браузер відповідає за paint. Render не обов’язково змінює DOM.

Це легко побачити: розмістіть годинник, який оновлюється щосекунди, поруч із текстовим полем і почніть вводити текст. Компонент рендериться знову, але текст і позиція курсора зберігаються. React порівнює результат і не чіпає поле без потреби.

Кожен рендеринг — незмінний знімок

У межах одного виклику компонента його props і значення стану — знімок, визначений до цього виклику. Виклик setter не змінює вже прочитану змінну.

function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <button
      onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}
    >
      {number}
    </button>
  );
}

Після одного натискання значення стане 1, а не 3.

Підставте значення number вручну. У цьому рендерингу воно дорівнює 0, тож три виклики означають setNumber(0 + 1): тричі запитано значення 1.

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

setNumber((n) => n + 1);
setNumber((n) => n + 1);
setNumber((n) => n + 1);

Отже, стан живе не всередині виклику функції. Функція — обчислення, яке читає його зі структур React. Далі ми дамо цим структурам назви й побачимо, чому існують дві версії дерева.

Дві частини React

React має дві частини, і це реальний архітектурний поділ.

Reconciler визначає, що змінилося. Він працює з компонентами, станом і пріоритетами, але не прив’язаний до DOM.

Renderer застосовує результат до конкретного середовища: react-dom — до браузерного DOM, react-native — до нативних представлень. Інші renderer можуть працювати з canvas, PDF, терміналом або тестовим виводом.

Тому react-reconciler є окремим пакетом, а React може працювати й у браузері, й на телефоні. Майже вся ця стаття присвячена reconciler — частині, яка не зобов’язана знати, що таке <div>.

Через це назва «віртуальний DOM» не зовсім точно описує Fiber. Reconciler зберігає дерево роботи, а вже конкретний renderer перетворює результат на DOM-вузли.

Частина 2. Навіщо React знадобився Fiber

Стек викликів не можна довільно призупинити

До React 16 рендеринг спирався на рекурсію: React обробляв корінь, його дітей і далі все дерево в одному безперервному стеку викликів.

Рекурсія природно підходить для дерева, але стек JavaScript має виконатися до спорожнення. Не можна довільно поставити звичайний рекурсивний виклик на паузу, повернути керування браузеру, а потім відновити його з того самого місця.

Це важливо для головного потоку браузера: JavaScript, значна частина обробки взаємодій і підготовка відображення конкурують за нього. Поки стек зайнятий 200 мс рендерингу, обробники нових натискань і введення змушені чекати.

Ключова проблема була не лише у швидкості React, а в неможливості поступитися керуванням посеред цієї роботи.

рекурсіяцикл об’єктівApp()Layout()List()Row()...потік зайнятий, 200 мснатискання очікує завершенняworkInProgressпозиція в одній зміннійпауза для взаємодії

Обсяг роботи подібний, але праворуч її можна розділити паузами: позиція зберігається в об’єкті, а не в незавершеному рекурсивному стеку.

Fiber — фрейм роботи, який можна відкласти

Рішення — не покладатися на стек JavaScript для збереження всього обходу, а представити роботу звичайними об’єктами.

Фрейм стека зберігає функцію, аргументи, місце повернення та проміжний результат. Fiber представляє подібну інформацію полями об’єкта в пам’яті. React може зберегти посилання на нього, зупинити свій цикл і повернутися до роботи пізніше.

Це порівняння є в самому коді React, у ReactInternalTypes.js, і збереглося після багатьох змін реалізації.

Один fiber приблизно відповідає одиниці роботи над компонентом або іншим вузлом React. Цикл обробляє їх по одному й між ними може вирішити, що час віддати потік браузеру.

Що насправді містить fiber

Нижче — поля конструктора FiberNode у packages/react-reconciler/src/ReactFiber.js для версії 19.3.0. Групування з вихідного коду допомагає зрозуміти їхнє призначення.

// what this fiber is
tag;
key;
elementType;
type;
stateNode;

// where it sits in the tree
return;
child;
sibling;
index;

// the work itself
ref;
refCleanup;
pendingProps;
memoizedProps;
updateQueue;
memoizedState;
dependencies;
mode;

// what to do at commit time
flags;
subtreeFlags;
deletions;

// priority
lanes;
childLanes;

// the other copy of this fiber
alternate;

Чотири поля варто пояснити окремо — саме їх часто розуміють неправильно.

stateNode посилається на конкретний екземпляр. Для host fiber на кшталт div це DOM-вузол. Для класового компонента — екземпляр класу. Для функціонального компонента зазвичай null: окремого екземпляра функції-компонента немає.

memoizedProps — властивості, з якими React завершив попередню роботу над fiber. pendingProps — властивості поточного рендерингу. Їхня ідентичність бере участь у перевірках можливості пропустити роботу; memo також має власне порівняння властивостей.

memoizedState залежить від типу вузла. Для класу це об’єкт стану, для функціонального компонента — початок зв’язаного списку записів хуків. Саме з порядком цих записів пов’язані правила хуків.

alternate посилається на іншу версію цього fiber. Навіщо їх дві — у частині 3.

Зверніть увагу на відсутні поля: тут немає output, pendingWorkPriority, firstEffect, lastEffect або nextEffect. Вони трапляються в старих статтях, але не описують цю версію React.

Три посилання, з яких складається дерево

Ця частина часто дивує, хоча саме вона багато що пояснює.

Батьківський fiber не зберігає масив усіх дітей. child посилається лише на першу дитину, решта пов’язана через sibling. Кожен вузол має return, що веде до батька.

AppHeaderMainLogochildsiblingreturnApp.child — Header; прямого посилання на Main немає.

Посилання на першу дитину, сусіда й батька — три поля з’єднують усе дерево.

App безпосередньо посилається на Header, але не на Main. Щоб дістатися до Main, потрібно пройти через App.child, а потім sibling.

Така структура спрощує паузу. React достатньо зберегти посилання на поточну одиницю роботи. Не потрібно зберігати весь рекурсивний стек або індекс у масиві: позицію обходу представляє сам fiber.

Униз із beginWork, угору з completeWork

Керування обходом організоване циклом while, а не рекурсією всього дерева. Основний цикл у ReactFiberWorkLoop.js дуже короткий.

На кожній ітерації performUnitOfWork обробляє вузол із workInProgress. Далі є два напрямки роботи.

beginWork іде вниз: викликає функцію компонента, визначає дітей, створює або повторно використовує їхні fibers і повертає першу дитину як наступну одиницю роботи.

Коли дітей немає, completeWork іде вгору. Для нових host fibers тут через createInstance створюються справжні, але ще від’єднані DOM-вузли й до них приєднуються готові діти. Це ще не вставлення у видиму сторінку. Далі обхід переходить до сусіда або завершує батьківський вузол.

AppHeaderMainLogo1 begin2 begin3 complete4 complete5 begin6 completeсуцільна — початок роботи; пунктир — завершення

Під час повного обходу вузол має початок роботи на шляху вниз і завершення на шляху вгору.

Тут є дві деталі, які дивують навіть досвідчених користувачів React.

По-перше, нові DOM-вузли можуть створюватися під час render. Вони від’єднані від документа, тому це ще не зміна видимого інтерфейсу.

По-друге, на початку beginWork є перевірки bailout. Ідентичність oldProps === newProps — порівняння посилань, не глибоке порівняння — разом із відсутністю запланованих оновлень і змін context може дозволити пропустити компонент або його непотрібну роботу.

Це пояснює, чому передавання {children} часто допомагає. Якщо елемент children створив предок, який не рендерився повторно, його посилання може залишитися тим самим, навіть коли проміжний компонент оновлюється. React отримує можливість пропустити незмінну гілку без окремого memo.

Частина 3. Два дерева

current and workInProgress

Для компонента React може підтримувати дві версії fiber: поточну та робочу. Робоча копія створюється за потреби й надалі використовується повторно.

Дерево current описує поточний зафіксований інтерфейс. Корінь посилається на нього через root.current.

Друге дерево — workInProgress, яке React зараз готує. Поле alternate пов’язує відповідні версії вузлів в обох напрямках.

Під час підготовки нового інтерфейсу React не застосовує його напівготові зміни до видимого DOM. Він будує результат збоку. Це дає три важливі властивості.

Якщо приходить терміновіша робота, незавершений рендеринг можна відкинути без відкочування видимого інтерфейсу.

Якщо рендеринг завершується помилкою, до обробки помилки користувач усе ще бачить попередній зафіксований результат.

Коли нове дерево готове й зміни зафіксовано, React перемикає root.current = finishedWork. Попередня версія стає запасною, і її об’єкти можуть використовуватися в наступному рендерингу замість зайвих алокацій.

currentworkInProgressна екраніу роботіAppNavListAppNavListalternateroot.current = finishedWorkодне присвоєння міняє ролі деревпопереднє дерево можна повторно використати

React чергує поточне й робоче дерева та повторно використовує об’єкти там, де зберігається ідентичність.

Як React визначає ідентичність компонента

Цей розділ особливо практичний: він пояснює помилку, з якою стикається майже кожна React-команда.

Під час узгодження React має зіставити старі fibers із новими елементами. Для цього враховуються ключ і тип у межах відповідного батьківського вузла.

Ключ, якщо він заданий, і тип — функція компонента або назва host-елемента.

Якщо ідентичність збігається, React повторно використовує fiber, оновлює props і зберігає пов’язаний стан, хуки та потрібні DOM-вузли.

Якщо ключ або тип означають іншу ідентичність, React демонтує старе піддерево й створює нове. Стан скидається, DOM може бути створений заново, а ефекти очищаються й запускаються для нових компонентів.

Позиція теж важлива: React зіставляє дітей одного батька, використовуючи ключі або позиції, коли ключів немає. Звідси класичний несподіваний результат:

{
  isEditing ? <Input value={text} /> : <Input value={text} />;
}

Той самий компонент на тій самій позиції зберігає стан. Зазвичай це бажано, але інколи — саме те, чого ви не хочете.

{
  isEditing ? <Input key="edit" /> : <Input key="view" />;
}

Різні ключі змушують React створити нове піддерево й скинути стан. Ключ — підтримуваний спосіб сказати: «це вже інша сутність, попередній стан більше не потрібен».

Тому key={index} у змінюваному списку може ламати поведінку. Після видалення першого запису колишній елемент 1 стає елементом 0. React бачить старий ключ 0 і той самий тип, зберігає стан, але передає нові дані. Прапорці й поля можуть опинитися не на тих рядках. Стабільний id прив’язує ідентичність до даних, а не до позиції.

Що залишається після completeWork

Під час повернення вгору completeWork збирає інформацію про потрібні зміни. flags — бітова маска для позначок на кшталт вставлення, оновлення, ефекту чи роботи з ref.

Старі статті описували effect list — окремий зв’язаний список лише тих fibers, які потребують роботи, побудований через firstEffect, lastEffect і nextEffect.

У React 19.3 цих полів немає; це можна перевірити в ReactFiber.js.

Замість них є subtreeFlags. Коли вузол завершується, bubbleProperties об’єднує прапорці його дітей через побітове OR і зберігає підсумок на батьку. Кожен батько знає, чи є потрібна робота в його піддереві.

Під час commit React обходить дерево згори й перевіряє subtreeFlags. Якщо потрібної роботи нижче немає, цілу гілку можна пропустити.

Старий список було зручно обходити, але його потрібно було будувати й підтримувати. Новий підхід збирає маски під час повернення вгору й використовує бітові перевірки, щоб пропускати непотрібні піддерева.

Частина 4. Commit

Три підфази та моменти виконання ефектів

Під час commit React застосовує зміни до DOM. У класичному описі є три послідовні підфази. Їхній порядок пояснює багато ситуацій, коли ефект бачить попереднє значення.

часдо mutationmutationlayoutpaintpassiveзмінаroot.currentgetSnapshotBeforeUpdateзміни DOMuseInsertionEffectuseLayoutEffect cleanupcomponentWillUnmountrefs від’єднаноuseLayoutEffect setupcomponentDidMountcomponentDidUpdaterefs приєднанобраузермалюєuseEffectочищенняпотім запускосновні зміни виконуються узгоджено

Layout-ефекти виконуються до paint. Пасивні ефекти зазвичай обробляються пізніше, але не мають абсолютної гарантії «лише після paint».

На схемі варто окремо відзначити три деталі.

Перемикання дерева відбувається між mutation і layout. Це дозволяє componentWillUnmount працювати зі старим деревом, а componentDidMount — уже з новим.

Старі refs від’єднуються під час mutation, нові приєднуються під час layout. Не слід читати ref.current для обчислення результату render: він може бути null або містити попереднє значення.

Пасивні ефекти useEffect обробляються окремо від основних DOM-mutation і layout-ефектів. Зазвичай браузер отримує можливість намалювати кадр раніше, але це не абсолютна гарантія «завжди після paint». Якщо виміряти DOM у пасивному ефекті й лише потім змінити стан, користувач може побачити проміжне компонування. Для вимірювання, яке має завершитися до paint, призначений useLayoutEffect, але він блокує відображення.

«Commit атомарний» — правило з уточненнями

Роками правило звучало просто: render можна перервати, commit — ні.

Для точного опису сучасної реалізації цього формулювання вже недостатньо.

Оригінальна стаття описує реалізацію React 19.3 із <ViewTransition>. Щоб анімувати перехід, React взаємодіє з браузерним View Transition API й очікує його колбеки. Тому робота commit організована через flushMutationEffects, flushLayoutEffects та інші функції, а внутрішня машина станів відстежує поточний етап. За наявності pending commit hasPendingCommitEffects не дозволяє почати новий рендеринг.

Для звичайного коду принцип лишається тим самим: інша React-робота не вклинюється довільно в незавершений commit. Але реалізація переходу може рознести його етапи між асинхронними колбеками, а не виконати все одним блоком.

Частина 5. Пріоритети

Саме система пріоритетів найбільше змінилася порівняно зі старими матеріалами про Fiber. Спочатку розберімо два популярні міфи.

Чому scheduler — не requestIdleCallback і не бюджет кадру

Знайоме пояснення звучить так: екран оновлюється 60 разів на секунду, кадр триває 16,7 мс, а React працює до вичерпання цього бюджету й потім повертає кадр браузеру.

Це не точний опис роботи React; частина такої моделі ніколи не відповідала опублікованій реалізації.

Опублікований scheduler React не спирається на requestIdleCallback. Після ранніх експериментів із цим API команда створила власний механізм. У браузері він, зокрема, планує завдання через MessageChannel; у різних середовищах можуть використовуватися інші доступні примітиви.

У реалізації, яку аналізує стаття, scheduler використовує базовий часовий зріз у п’ять мілісекунд, а не обчислює залишок кожного кадру.

// packages/scheduler/src/SchedulerFeatureFlags.js
export const frameYieldMs = 5;

П’ять, а не шістнадцять. Ідея — регулярно давати браузеру можливість обробити іншу роботу, не прив’язуючись до однієї перевірки на кадр.

Не підлаштовуйте продуктовий код під це число. Це внутрішня константа конкретної реалізації scheduler, а не контракт публічного API.

Стаття також описує раннє повернення керування, якщо через enableRequestPaint запитано paint. Важлива історична деталь: у React 18 requestPaint в опублікованому scheduler був порожньою функцією; пізніше його поведінка змінилася. Такі прапорці завжди потрібно читати в контексті конкретної версії збірки.

Повернути керування дешево завдяки структурі з частини 2. Позиція React зберігається посиланням у змінній: пауза означає вихід із циклу, продовження — повторний вхід. Не потрібно зберігати рекурсивний стек усього дерева.

Lanes

Fiber має lanes і childLanes. Окрема lane представлена бітом у масці, яку React організує як 31-бітове ціле число.

Представлення просте; цікавіша причина його появи.

Стара система представляла пріоритет оновлення числом. Термінова синхронна робота, введення користувача та фонові оновлення мали певний порядок. Щоб визначити належність до поточного набору роботи, порівнювали числові пріоритети.

Але одного впорядкованого числа замало, коли менш пріоритетна робота очікує дані через Suspense, а інша вже готова. React потрібна можливість сказати: «рендерити ці кілька незалежних оновлень разом, але не те». Діапазон чисел не виражає довільну множину.

Це можна представити через Set, але маска бітів дає компактне зберігання та дешеві перевірки без окремих об’єктів для набору.

біт 0біт 3014 transition lanes4 retry lanesIdleOffscreenSyncInputContinuousDefaultщедваблокувальна роботаконкурентна робота з паузамиgetHighestPriorityLane(lanes)return lanes & -lanes;бітова операція без обходу набору31 біт: знаковий біт не використовується для lanes

Замість одного числового пріоритету — набір бітів, у якому легко перевірити перетин роботи.

Звідси дві важливі властивості.

Перетин наборів перевіряється через (lanes & batch) !== 0. Наймолодший установлений біт, що відповідає найвищій lane у цій схемі, можна виділити виразом lanes & -lanes.

Один рендеринг може об’єднати кілька lanes, які не утворюють суцільного діапазону числових пріоритетів.

Lanes з’явилися в React 17, хоча цей реліз часто пам’ятають як версію «без нових можливостей». Йшлося про відсутність нових видимих API, а не про відсутність внутрішніх архітектурних змін.

Строки виконання як запобіжник

Маски описують набір і пріоритет роботи, але самі не вимірюють час. Lane могла б постійно програвати терміновішим оновленням. Це називається starvation — голодуванням роботи.

У корені є expirationTimes із записом для lane. markStarvedLanesAsExpired перевіряє очікування і може позначити роботу простроченою, щоб вона виконалася без подальшого відкладання. У розглянутій реалізації використовуються, зокрема, пороги порядку 250 мс для термінових категорій та 5 с для transitions; конкретна поведінка залежить від виду lane й feature flags.

Отже, строки виконання не зникли: вони стали запобіжником від голодування, а не основним способом опису наборів роботи.

Який рендеринг справді поступається керуванням

Це одна з найважливіших практичних поправок до спрощених пояснень.

Фраза «у React 18 весь рендеринг став переривним» надто широка. Конкурентна поведінка залежить від виду оновлення, його lane, кореня та способу планування.

Рішення про time slicing приймає внутрішня логіка на кшталт shouldTimeSlice. Блокувальна lane, прострочена робота або flushSync можуть вимагати виконання одним проходом.

У розглянутій збірці DefaultLane належить до блокувальних категорій. Водночас не всі звичайні оновлення потрапляють саме в неї: наприклад, дискретна подія на кшталт click може отримати синхронний пріоритет. Тому не варто виводити lane лише з назви setState або припускати, що будь-яке оновлення автоматично поступатиметься керуванням.

Time slicing насамперед стосується transitions, повторних спроб Suspense, відкладених значень, прихованої та фонової роботи — з урахуванням конкретного пріоритету й стану scheduler.

Якщо звичайне термінове оновлення рендерить велике дерево й інтерфейс підгальмовує, сама наявність підтримки concurrency не зробить цю роботу переривною.

Під час читання коду важливо враховувати feature flags. Стаття звертає увагу на workLoopConcurrent і workLoopConcurrentByScheduler: наявність функції в репозиторії ще не означає, що саме вона використовується в опублікованій збірці. Перевіряйте активні гілки вашої версії React.

Чого concurrency не може

Оскільки стаття слугує довідником, обмеження варто сформулювати так само чітко, як переваги.

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

Concurrency не робить довільні DOM-mutation переривними. Десять тисяч вставлень усе одно можуть надовго зайняти головний потік.

Вона також не розділить важке browser layout, paint або синхронне тіло вашого ефекту.

Поділ роботи має накладні витрати й іноді потребує повторних обчислень. Мета concurrency — чутливий інтерфейс під час виконання, а не гарантоване скорочення загального часу рендерингу.

Незалежні transitions у версії 19.3

У старіших реалізаціях кілька transitions могли об’єднуватися в одну пов’язану роботу, тому повільний перехід затримував інші.

Оригінальна стаття описує зміну React 19.3 через enableParallelTransitions: transitions можуть рендеритися незалежніше. Це версійно залежна деталь, а не обіцянка для всіх React-збірок.

Pull request 2020 року, який вводив lanes, уже згадував таке покращення як майбутню роботу. Іноді розвиток архітектури — це зняття старого обмеження, а не додавання нового публічного API.

Практичне зауваження: документація, changelog і вихідний код можуть оновлюватися неодночасно. Звіряйте застереження про групування transitions саме з тією версією React, яку використовуєте.

Частина 6. Публічні API через призму lanes

Тут розглянемо знайомі публічні API через щойно описані механізми планування.

Transitions і поширена помилка

Почнімо з особливо поширеного непорозуміння.

startTransition не відкладає передану йому функцію. React викликає її одразу й синхронно. Як transition позначаються оновлення стану, заплановані в її відповідному контексті.

startTransition(() => {
  // this runs NOW, on the main thread, and blocks it
  const filtered = items.filter(expensiveFilter);

  // only THIS is marked as a transition
  setResults(filtered);
});

Якщо expensiveFilter займає 200 мс, саме обгортання в startTransition не допоможе: фільтрування виконається відразу й заблокує потік. Нижчий пріоритет отримає спричинений setResults рендеринг, а не вже виконане обчислення.

Потрібно здешевити або винести важке обчислення. Transition — позначка оновлення, а не затримка для довільного коду.

Внутрішня реалізація роздає transitions lanes із виділеного набору по колу. Стаття описує поділ цього набору в React 19.2: окремі біти для startTransition і useDeferredValue, щоб ці категорії не конкурували за ті самі lanes. Це внутрішня деталь конкретної версії.

React 19 розширив підхід через Actions: для async-функції isPending може залишатися істинним протягом усієї дії. Але після await оновлення стану все ще може потребувати нового startTransition, щоб зберегти transition-контекст.

useDeferredValue

useDeferredValue нагадує debounce, але працює інакше.

Debounce чекає задану затримку. useDeferredValue не має такого таймера: терміновий рендеринг використовує попереднє значення, а фоновий — намагається підготувати нове. Якщо до завершення надходить ще одне оновлення, фонову роботу можна відкинути й почати заново.

Тут немає фіксованої затримки для налаштування. Фонова робота поступається терміновому введенню, а після паузи отримує можливість завершитися.

Два застереження: усередині transition додаткова відкладеність зазвичай не потрібна, бо робота вже нетермінова. А новий об’єкт, створений під час кожного рендерингу, може спричиняти зайві фонові спроби, оскільки значення постійно має нове посилання.

Автоматичне пакетування

Кілька setter-викликів у одному обробнику React може об’єднати в один рендеринг.

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

У React 17 пакетування насамперед працювало всередині обробників подій React. React 18 із новим коренем поширив автоматичне пакетування також на таймери, Promise-колбеки й інші сценарії.

Під час переходу на React 18 важливим був createRoot, а не лише номер пакета: старий ReactDOM.render зберігав попередню поведінку. У React 19 ReactDOM.render прибрали.

React не об’єднує незалежні навмисні взаємодії користувача в одну подію: між окремими натисканнями мають застосуватися потрібні зміни інтерфейсу, наприклад блокування кнопки відправлення.

Suspense та Activity

Обидві можливості використовують здатність зберігати дерево роботи, змінювати його пріоритет і повторно використовувати стан.

Suspense з’явився в React 16.6 для code splitting. У React 18 він отримав потоковий серверний рендеринг і вибіркову гідратацію: React може раніше гідратувати частину, з якою взаємодіє користувач. React 19 також змінив деякі аспекти показу fallback і продовження роботи.

Activity з’явився в React 19.2. Він дозволяє приховати піддерево, зберігши стан і DOM, але очистивши ефекти й знизивши пріоритет прихованих оновлень. Так вкладка може зберегти введені дані та прокрутку. Це не означає, що приховане дерево зовсім безкоштовне; мета — керувати його життєвим циклом без повної втрати стану.

Чому компонент знову рендерився?

На практиці команда часто запитує саме це. Більшість випадків пояснюють чотири причини.

Змінився власний стан. React може порівняти нове значення з поточним і пропустити зайву роботу, якщо вони однакові. Тому setCount(5) за поточного значення 5 часто нічого не змінює. Але не слід будувати логіку на точній кількості викликів функції компонента.

Батько рендерився знову й передав нові props. Перевірки bailout враховують ідентичність властивостей. Inline-об’єкт, масив або стрілкова функція створюють нове посилання щоразу.

// new object and new function on every parent render
<Child style={{ margin: 8 }} onSave={() => save(id)} />

Тому новостворений об’єкт часто нівелює memo: стандартне порівняння властивостей не вважає два різні об’єкти однаковими лише через однаковий вміст.

Змінився context, який компонент читає. React відстежує залежності споживачів context через відповідні структури fiber. Оновлення context може пройти крізь memo, адже порівняння props не скасовує підписку на context.

Типовий випадок — provider з inline-об’єктом value: нове посилання на кожному рендерингу оновлює всіх споживачів. Часто варто розділити великий context на незалежні менші або стабілізувати значення, а не додавати мемоізацію навмання.

Компонент потрапив у повторний обхід батьківського дерева. Тут допомагає композиція через {children}: якщо елемент створений вище й зберіг своє посилання, оновлення проміжного батька може не вимагати повторної роботи цієї гілки. Власний стан або context children усе одно можуть її оновити.

Проблема tearing і зовнішні сховища

Переривний рендеринг створив нову проблему й пояснює появу useSyncExternalStore.

Якщо React зупинив рендеринг, а зовнішнє сховище змінилося, одні компоненти могли прочитати старий знімок, а інші — новий. Один екран показує дві версії тих самих даних. Це називають tearing.

Станом React керує сам, а стороннє сховище може змінитися поза його контролем. useSyncExternalStore дає підтримуваний спосіб підписки й перевірки знімка перед commit. Якщо він змінився під час роботи, React може повторити потрібний рендеринг. Саме для сумісності з конкурентним рендерингом бібліотеки сховищ адаптували свої інтеграції.

Для підписки на зовнішнє джерело використовуйте цей API. getSnapshot має повертати стабільний знімок, доки дані не змінилися. Новий об’єкт на кожному виклику виглядатиме як безперервні зміни й може спричинити цикл.

Частина 7. Що змінилося і як це впливає на ваш код

Що застаріло в попередніх статтях

Документ Ендрю Кларка 2016 року й досі добре пояснює, навіщо потрібен Fiber. Але його історичні назви полів не слід сприймати як опис сучасного React. Ось відповідності:

У старих статтяхУ розглянутій сучасній реалізації
pendingWorkPriority, одне числоlanes та childLanes, бітові маски
Упорядковані числові пріоритетиНабори lanes із власним порядком вибору
requestIdleCallbackВласний scheduler, зокрема MessageChannel
Бюджет кадру 16 мсБазовий зріз frameYieldMs близько 5 мс
Окремий effect listsubtreeFlags для пропуску гілок
Строки як головний schedulerСтроки як захист від starvation
Один перемикач Concurrent ModeПоведінка залежить від оновлення й API
effectTagflags
Поле output у fiberТакого поля немає; вузли готує completeWork
Fiber ще переписуютьОсновна архітектура в React із версії 16
Кожен render переривнийНе всі lanes допускають time slicing
ReactDOM.rendercreateRoot; старий API прибрано в React 19

Структура й аргументація документа залишаються корисними: fiber нагадує фрейм стека в купі, дерево з’єднується через три посилання, є поточна й робоча версії, а обхід ділиться на одиниці роботи. Читайте заради ідей, не назв полів.

Fiber та React Compiler розв’язують різні завдання

Після стабілізації React Compiler у жовтні 2025 року закономірно виникло запитання: чи замінює він Fiber? Ні.

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

Compiler аналізує компоненти під час збірки й додає код мемоізації, який інакше довелося б писати вручну. Самі результати обчислень кешуються не під час збірки, а під час виконання, зокрема через внутрішній useMemoCache.

Оскільки compiler перетворює код, він може розміщувати перевірки кешу точніше, ніж ручні виклики хуків. Якщо безпечність оптимізації компонента не доведена, він може пропустити цей компонент, а не зламати всю збірку.

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

Як побачити це самостійно

Назви описаних структур можна знайти у вихідному коді та налагоджувачі.

Відкрийте React DevTools і виберіть компонент. Інструмент читає дані Fiber; показаний список хуків пов’язаний із записами memoizedState.

У development-збірці поставте breakpoint у beginWork в модулі reconciler, наприклад ReactFiberBeginWork.js, і спричиніть оновлення. Можна простежити виклики з ReactFiberWorkLoop.js й пройти обхід по одному fiber.

React 19.2 додав Performance Tracks до інструментів Chrome: треки Scheduler допомагають відрізняти блокувальну та transition-роботу. Це зручніше для практичної діагностики, ніж безпосереднє читання бітових masks lanes.

React 19.1 додав Owner Stacks — development-трасування того, який компонент відрендерив інший, доступне через captureOwnerStack.

Практичні наслідки для вашого коду

Практичних правил менше, ніж може здатися з довжини статті.

Не оцінюйте швидкодію лише кількістю render. Вимірюйте тривалість роботи й реальні зміни інтерфейсу; рендеринг без DOM-змін теж може бути дорогим, якщо дорогий ваш код.

Пам’ятайте: функція всередині startTransition починається зараз. Важке обчислення потрібно здешевити або винести, а не просто обгорнути позначкою пріоритету.

Не очікуйте, що будь-який setState автоматично стане переривним. Поведінку визначають пріоритет і спосіб планування оновлення.

Використовуйте key свідомо: він визначає ідентичність і дозволяє навмисно скинути стан.

Беріть useLayoutEffect, лише якщо потрібно синхронно прочитати або змінити layout до paint. Для звичайної синхронізації із зовнішніми системами починайте з useEffect.

Не оптимізуйте застосунок під внутрішню константу 5 мс. Це деталь scheduler, а не налаштування вашого продукту.

Що почитати далі

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

A deep dive into React Fiber на LogRocket містить матеріал про різні покоління React. Розбір старого коду та поля на кшталт output потрібно читати як історію, а не поточний контракт.

Render and Commit — офіційний вступ до теми першої частини. Поруч є корисні сторінки про стан як знімок і збереження та скидання стану.

React Fiber Architecture автора xubh докладно розглядає структури даних; його зручно читати поруч із кодом.

Для деталей, що змінюються, читайте changelog React і packages/react-reconciler/src потрібного тегу версії. Саме код цієї версії визначає реалізацію; гілка main може вже відрізнятися від установленого пакета.

Дякую за читання! Сподіваюся, стаття допомогла зрозуміти React глибше.