Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «React fiber та процес оновлення віртуального DOM». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**React Fiber** - це механізм узгодження React 16+, реалізований як зв'язний список fiber-вузлів з вказівниками `child`, `sibling` та `return`. ```jsx const [isPending, startTransition] = useTransition(); startTransition(() => setHeavyList(newList)); // оновлення з низьким пріоритетом ``` **Ключове:** фаза рендерингу переривається; фаза коміту завжди синхронна і атомарна.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**React Fiber** - це переосмислення рушія узгодження (reconciliation engine) у React 16, реалізоване як зв'язний список, що дозволяє рендерити віртуальний DOM інкрементально, з паузами і пріоритетами. ## Теорія ### TL;DR - Fiber - це конвеєр з кнопками паузи: старий React обходив усе дерево компонентів за один синхронний прохід (блокував браузер на 100мс+ при великих деревах); Fiber призупиняє другорядну роботу, щоб не гальмувати дії користувача. - Головна різниця: стековий рекурсивний обхід (до 16 версії) проти зв'язного списку fiber-вузлів. Зв'язний список можна зупинити посередині і продовжити з тієї самої точки. - Кожен fiber-вузол має три вказівники: `child` (перший дочірній), `sibling` (наступний сусід), `return` (батько). - `createRoot` у React 18 вмикає повний concurrent-режим; `ReactDOM.render` залишається на старому синхронному шляху. - Практичне правило: великі списки (500+ елементів) або анімації поряд із завантаженням даних - використовуй `startTransition` або `useDeferredValue`. ### Швидкий приклад ```jsx import { useState, useTransition } from 'react'; function App() { const [input, setInput] = useState(''); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { setInput(e.target.value); // високий пріоритет: виконується одразу startTransition(() => { filterLargeList(e.target.value); // низький пріоритет: Fiber ставить у чергу }); }; return <input value={input} onChange={handleChange} placeholder="Пиши без затримок" />; } ``` `startTransition` повідомляє Fiber, що це оновлення може зачекати. Поле вводу залишається миттєвим, бо Fiber передає важку роботу браузеру між 16мс-кадрами і відновлює її тільки коли головний потік вільний. ### Стековий рекурсор vs Fiber До React 16 рекурсор обходив дерево компонентів через стек викликів - один великий синхронний прохід. Дерево з 1000 вузлів по 0.1мс кожен блокувало браузер на 100мс. Жодних пауз, жодної поступки. Fiber замінює стек зв'язним списком. React обходить його через `nextUnitOfWork`, обробляє 5-10мс роботи за один шматок, потім перевіряє чи браузеру потрібно щось намалювати або обробити подію. Якщо так - `shouldYield()` повертає `true`, і React передає керування. Потім підхоплює з `nextUnitOfWork` рівно звідти, де зупинився. Дерево в процесі роботи (work-in-progress) - це `current.alternate`, клон поточного дерева, тому інтерфейс залишається стабільним поки React будує наступну версію. Ось і весь архітектурний зсув. ### Фаза рендерингу: побудова work-in-progress дерева Кожна зміна стану через `useState` або `setState` потрапляє у чергу оновлень прикріплену до fiber-вузла. React читає цю чергу і починає будувати WIP-дерево: клон поточного fiber-дерева з новим станом. Під час цієї фази React викликає твої render-функції і запускає `reconcileChildFibers`, щоб порівняти нові та старі пропси і стан. Якщо типи елементів збігаються - патчить існуючий вузол. Якщо тип змінився (наприклад, `div` на `span`) - знищує гілку і будує нову з нуля. Тут важливі ключі. Для списків `key` дозволяє React зіставляти старі і нові вузли за ідентичністю, а не позицією. Без ключів перестановка двох елементів виглядає як два окремі зміни. Фаза рендерингу чиста і переривається. Жодних змін у реальному DOM тут не відбувається. ### Фаза коміту: атомарна і невід'ємна Коли WIP-дерево готове, React переходить до фази коміту. Цю частину не можна перервати. Три підфази виконуються по черзі: - `getSnapshotBeforeUpdate` - зчитує DOM до будь-яких мутацій. - `commitMutationEffects` - додає, видаляє і змінює реальні DOM-вузли. - `commitLayoutEffects` - синхронно запускає `componentDidMount`, `componentDidUpdate` та `useLayoutEffect`. Після того як браузер намалює кадр, асинхронно виконується `useEffect`. Фаза коміту завжди синхронна. На практиці саме це найчастіше дивує розробників: навіть у concurrent-режимі, якщо в один коміт потрапляє 10 000 реальних DOM-вузлів, браузер блокується. Fiber розбиває побудову WIP-дерева, але не саме записування в DOM. Тому віртуалізація через `react-window` важлива навіть у React 18. ### Як Fiber планує роботу Fiber не використовує `requestIdleCallback` напряму - підтримка у браузерах непослідовна, а мінімальна затримка у 50мс надто велика. Натомість пакет `scheduler` полімає його через цикли `MessageChannel` `postMessage`, нарізаючи роботу на шматки по ~5мс. Функція `shouldYield()` перевіряє дедлайн; коли час вийшов - поточна одиниця роботи ставиться на паузу і керування повертається браузеру. Пріоритети кодуються через 32-бітні lane-маски. `SyncLane` (біт 0) - для введення користувача. Вищі біти відповідають роботі з нижчим пріоритетом: переходи (transitions), відкладені значення, фонові задачі. Коли `startTransition` обгортає оновлення стану, React додає `TransitionLane` у поле `lanes` fiber-вузла через побітове АБО і розповсюджує його вгору деревом. Менший номер біту означає вищий пріоритет. ### Коли використовувати concurrent-функції Великі списки або таблиці (500+ елементів): обгортай важке оновлення стану у `startTransition`, щоб уникнути затримки введення. Анімації поряд із дорогим перерендером: `useDeferredValue` тримає lane анімації окремо від lane повільних даних. Прості застосунки з невеликим деревом компонентів: стандартний синхронний режим підходить. Concurrent-режим додає накладні витрати планувальника без видимої користі при дереві менше 100 вузлів. Легасі-код на `ReactDOM.render`: такі проекти використовують старий синхронний шлях. Перехід на `createRoot` вмикає concurrent-функції, але деякі патерни з методами життєвого циклу і сторонніми бібліотеками поводяться інакше. ### Порівняльна таблиця | Функція | Стековий рекурсор (до 16) | Fiber (16+) | |---|---|---| | Внутрішня структура | Стек викликів | Зв'язний список (child / sibling / return) | | Переривання | Ні - один атомарний прохід | Так - пауза і відновлення по вузлах | | Рівні пріоритету | Немає | 32-бітна lane-маска (5+ рівнів) | | Планування | Хаки через setTimeout | Пакет `scheduler` через MessageChannel | | Concurrent-режим | Недоступний | Через `createRoot` | | Коли використовувати | Маленькі застосунки без анімацій | Продакшен з великими деревами | ### Типові помилки **Загортати все у `startTransition`.** Переходи відкладають перерендер, але оновлення стану самого поля вводу все одно має відбуватись синхронно. Якщо загорнути його у transition, поле починає лагати. ```jsx // Неправильно: поле вводу гальмує, бо setValue відкладено startTransition(() => setValue(e.target.value)); // Правильно: відкладаємо тільки важке вторинне оновлення setValue(e.target.value); // миттєво startTransition(() => setFiltered(computeFilter(e.target.value))); ``` **Думати що concurrent-режим прибирає весь гальмівний ефект.** Фаза коміту завжди синхронна. Якщо в один коміт потрапляє 20 000 DOM-вузлів, браузер блокується. ```jsx // Гальмує: 20 000 реальних вузлів за один коміт {items.map(item => <div key={item.id}>{item.text}</div>)} // Рішення: віртуалізація <FixedSizeList height={500} itemCount={20000} itemSize={35}> {Row} </FixedSizeList> ``` **Використовувати `flushSync` скрізь для "миттєвих" оновлень.** `flushSync` форсує синхронний рендер і повністю обходить планувальник Fiber. При частому використанні це викликає каскади гальм. Залишай його для випадків де треба прочитати layout одразу після зміни стану: фокус, прокрутка. **Ігнорувати подвійний виклик ефектів у StrictMode з async-переходами.** У режимі розробки StrictMode запускає ефекти двічі. `fetch` всередині `useEffect`, загорнутий у `startTransition`, викликається двічі. Без `AbortController` або прапора `cancelled` перша відповідь може перезаписати другу. ### Де це використовується - React 18 застосунки: `createRoot` + `startTransition` для миттєвого введення у великих списках задач. - Next.js 13+ app router: автоматичний concurrent-рендеринг; серверні компоненти стримують контент через Fiber-лейни. - Redux Toolkit: `useSelector` разом із `useDeferredValue` для адмін-панелей з великими оновленнями стору. - TanStack Query: `useSuspenseQuery` інтегрується з механізмом bailout у Fiber, скасовуючи застарілі запити при перериванні вищим пріоритетом. ### Питання на співбесіді **Q:** Опиши структуру зв'язного списку у Fiber. Які три вказівники є на кожному вузлі? **A:** Кожен fiber має `child` (перший дочірній компонент), `sibling` (наступний на тому ж рівні) і `return` (батько). Обхід: спочатку `workInProgress.child`; якщо немає - `workInProgress.sibling`; якщо немає - підіймаємось через `workInProgress.return` доки не знайдемо sibling. WIP-дерево - це `current.alternate`. **Q:** Як Fiber планує роботу без `requestIdleCallback`? **A:** Пакет `scheduler` використовує `MessageChannel` `postMessage` щоб отримати колбек після відмальовування браузера. Всередині `shouldYield()` перевіряє дедлайн ~5мс і повертає `true` коли час вийшов. Це дає тонший контроль ніж `requestIdleCallback` з його мінімумом 50мс. **Q:** Яка різниця між фазою рендерингу і фазою коміту? **A:** Фаза рендерингу чиста і переривається - будує WIP-дерево, порівнює fiber-вузли, збирає список ефектів. DOM не чіпається. Фаза коміту синхронна і атомарна - застосовує DOM-мутації, потім запускає методи життєвого циклу та ефекти у фіксованому порядку. **Q:** Поясни lane-маски. Покажи приклад із двома пріоритетами. **A:** Лейни - це 32-бітне ціле число. Біт 0 (значення 1) - `SyncLane` для введення користувача. Біт 4 (значення 16) - `TransitionLane` для `startTransition`. React додає новий лейн через побітове АБО у поле `lanes` fiber-вузла. Менший номер біту означає вищий пріоритет, тому введення завжди перериває перехід. **Q (senior):** Чому `useDeferredValue` не викликає нескінченний цикл перерендерів навіть у StrictMode? **A:** `useDeferredValue` планує перерендер із відкладеним значенням на низькому пріоритеті. Під час цього перерендеру відкладене значення вже збігається з поточним - React робить bailout, бо стан не змінився. StrictMode викликає render-функцію двічі, але обидва виклики поділяють той самий `current` fiber і однаково виходять через bailout. Нових оновлень не планується. **Q (senior):** Чому SSR не використовує concurrent-планування Fiber? **A:** SSR стримує HTML синхронно до клієнта. Там немає браузерного планувальника, якому можна поступитися - немає кадрів відмальовування, немає подій користувача. React використовує простий синхронний шлях на сервері. `ReactDOMServer.renderToString` обробляє все дерево за один прохід. ## Приклади ### Базовий: як Fiber поступається браузеру ```jsx import { useState, useTransition } from 'react'; function SlowList({ count }) { return ( <> {Array.from({ length: count }, (_, i) => ( <div key={i} style={{ padding: '4px', borderBottom: '1px solid #eee' }}> Елемент {i + 1} </div> ))} </> ); } function App() { const [count, setCount] = useState(100); const [isPending, startTransition] = useTransition(); return ( <div> <button onClick={() => startTransition(() => setCount(c => c + 500))} > Додати 500 елементів {isPending && '(рендер...)'} </button> {/* Fiber будує WIP-дерево у фоні, поступаючись кліками на кнопку */} <SlowList count={count} /> </div> ); } ``` `isPending` стає `true` поки Fiber будує WIP-дерево для нового count. Кнопка залишається клікабельною весь цей час. Без `startTransition` клік на кнопку блокував би UI до завершення коміту. ### Середній: `useDeferredValue` для фільтрації великого списку ```jsx import { useState, useDeferredValue, memo, useMemo } from 'react'; const ItemList = memo(({ filter, items }) => { const filtered = items.filter(item => item.label.toLowerCase().includes(filter.toLowerCase()) ); return ( <ul> {filtered.map(item => <li key={item.id}>{item.label}</li>)} </ul> ); }); function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); // копія з низьким пріоритетом const items = useMemo(() => generateItems(1000), []); return ( <> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Фільтр..." /> {/* Перерендериться з deferredQuery - завжди на крок позаду query */} <ItemList filter={deferredQuery} items={items} /> </> ); } ``` `deferredQuery` відстає від `query` на один або більше рендерів. Fiber планує перерендер `ItemList` на низькому пріоритеті. Поле вводу оновлюється на кожне натискання клавіші без очікування фільтрації. `memo` не дає `ItemList` перерендеритись коли змінився тільки `query`, але ще не `deferredQuery`. ### Просунутий: concurrent fetch у StrictMode з очищенням ```jsx import { useState, useEffect, useTransition } from 'react'; function DataFetcher() { const [data, setData] = useState(null); const [isPending, startTransition] = useTransition(); useEffect(() => { let cancelled = false; // StrictMode викликає цей ефект двічі у режимі розробки: // монтування -> очищення (cancelled = true) -> монтування знову // Без прапора перша відповідь може перезаписати другу startTransition(() => { fetch('/api/heavy-data') .then(res => res.json()) .then(json => { if (!cancelled) setData(json); // ігноруємо якщо ефект очищено }); }); return () => { cancelled = true; }; }, []); if (isPending) return <p>Завантаження...</p>; return <pre>{JSON.stringify(data, null, 2)}</pre>; } ``` `shouldYield` у Fiber також може перервати низькопріоритетний перехід якщо між стартом fetch і його завершенням прийде більш пріоритетне оновлення. Прапор `cancelled` обробляє обидва випадки: подвійний виклик від StrictMode і переривання від Fiber. Без нього отримаєш або застарілий стан, або попередження React про оновлення розмонтованого компонента.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.