nmk

Лекція №2 (2 години). Механізми роботи React: Virtual DOM та Reconciliation

План лекції

  1. Вступ до архітектурних принципів ефективного рендерингу
  2. Концепція Virtual DOM як інтелектуального прошарку
  3. Алгоритм Reconciliation: Математика та евристики
  4. Списки та магічна роль атрибуту key
  5. Еволюція архітектури від Stack до Fiber
  6. Нові можливості React - автоматизація та продуктивність
  7. Практичні поради щодо оптимізації рендерингу

Перелік умовних скорочень

Вступ

Одна з найскладніших задач у фронтенд-розробці — це ефективне оновлення інтерфейсу користувача у відповідь на зміну даних. Браузерний DOM неймовірно повільний при внесенні змін, оскільки будь-яка модифікація може викликати ланцюжок дорогих операцій: перерахунок стилів (Recalculate Style), оновлення макета (Reflow/Layout) та перемальовування пікселів (Repaint). У цій лекції ми заглибимося у внутрішню архітектуру React, яка дозволяє йому “обманювати” повільність DOM за допомогою концепцій Virtual DOM та Fiber.


1. Вступ до архітектурних принципів ефективного рендерингу

Існує кілька стратегій оновлення UI:

  1. Знищити і створити наново (Підхід старих шаблонізаторів): При будь-якій зміні ми повністю видаляємо старий HTML і вставляємо новий. Дорогий процес, що втрачає стан фокусу інпутів та прокрутки.
  2. Точкові оновлення (Data Binding): Спостереження (Observables) за кожною змінною (як працює Angular або Vue/Svelte). Потребує складного трекінгу залежностей.
  3. Віртуальний підхід React: Відмалювати оновлений інтерфейс віртуально, знайти різницю з поточним станом і застосувати лише цей “патч” змін до реального DOM.

2. Концепція Virtual DOM як інтелектуального прошарку

Virtual DOM — це концепція програмування, в якій ідеальне, або “віртуальне”, представлення інтерфейсу користувача зберігається в пам’яті (In-Memory) і синхронізується з “реальним” DOM.

По суті, елемент React — це просто звичайний (plain) JavaScript об’єкт. Його створення коштує дуже дешево.

Приклад: Коли ми пишемо такий JSX код:

const element = <div className="box">Hello</div>;

Babel конвертує його таким чином, щоб React створив об’єкт, який виглядає приблизно так:

// Спрощене подання React об'єкта
const virtualElement = {
  type: "div",
  props: {
    className: "box",
    children: "Hello",
  },
};

React тримає в пам’яті дерево таких об’єктів. Коли стан (state) змінюється, React створює нове, повністю незалежне дерево Virtual DOM.


3. Алгоритм Reconciliation: Математика та евристики

Як перетворити одне дерево (старе) на інше дерево (нове) з мінімальною кількістю операцій? У класичній інформатиці (алгоритм Левенштейна та алгоритми обчислення дерев) ця задача математично вирішується за час $O(n^3)$, де n — кількість елементів дерева.

Для типового додатка з 1000 компонентів це $1000^3$ (мільярд) порівнянь. Це неприпустимо довго.

Тому інженери React створили евристичний алгоритм порівняння (Reconciliation, або алгоритм “Diffing”), який зводить складність до $O(n)$. Алгоритм базується на двох фундаментальних припущеннях:

  1. Два елементи з різними типами (type, наприклад, <div> та <span> або <nav> та <header>) згенерують різні дерева.
    • Якщо тип елемента змінився (наприклад, стан привів до заміни <button> на <a>), React не намагається їх порівнювати. Він повністю знищує старе DOM-дерево (включно із вкладеними дітьми) та монтує нове дерево з чистого аркуша.
  2. Розробник може підказати, які елементи залишаються стабільними в списках за допомогою пропсу key.
    • Це дозволяє React розпізнавати “тих самих” дітей, навіть якщо їхній індекс у списку змінився.

Процес (якщо типи однакові):

// Старе:
<div className="before" title="stuff" />

// Нове:
<div className="after" title="stuff" />

React порівнює props, бачить, що title не змінився, а className змінився, і відправляє в реальний DOM точкову команду: element.className = 'after'.


4. Списки та магічна роль атрибуту key

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

Без key:

// Старе
<ul>
  <li>Яблуко</li>
  <li>Банан</li>
</ul>

// Нове (додали ківі на ПОЧАТОК списку)
<ul>
  <li>Ківі</li> <!-- React думає, що Яблуко перетворилось на Ківі -->
  <li>Яблуко</li> <!-- React думає, що Банан перетворився на Яблуко -->
  <li>Банан</li> <!-- React додає новий тег -->
</ul>

Результат: $100\%$ елементів мутували, інтерфейс страждає від перемальовок.

З key:

// Старе
<ul>
  <li key="apple">Яблуко</li>
  <li key="banana">Банан</li>
</ul>

// Нове
<ul>
  <li key="kiwi">Ківі</li>
  <li key="apple">Яблуко</li>
  <li key="banana">Банан</li>
</ul>

Тепер React бачить, що ключі “apple” і “banana” існують в обох деревах. Він лише вставляє новий елемент “kiwi” поверх старих. Жодних зайвих модифікацій існуючих вузлів!

Чому не можна використовувати index масиву як key? Якщо елементи можуть міняти порядок, сортуватися або видалятися із середини, index прив’язується не до даних, а до позиції. Це ламає логіку Reconciliation, оскільки React починає оновлювати неправильні компонети (баги зі станом інпутів у відсортованих списках).


5. Еволюція архітектури від Stack до Fiber

До версії React 16 алгоритм узгодження базувався на архітектурі, яка називалася Stack. Вона працювала синхронно (рекурсивно крокувала деревом). Це означало, що після початку потоку рендерингу (наприклад, через складний setState), браузер блокувався (main thread freeze) до тих пір, поки React не завершить обробку всього дерева.

Привіт, React Fiber (React 16+) Fiber — це повний перепис внутрішнього ядра (двигуна) React.

  1. Асинхронний рендеринг (Time-slicing): Fiber дозволяє “розрізати” роботу на маленькі кванти часу.
  2. Призупинення роботи (Yielding): React перевіряє “час” і, якщо час вичерпано (потрібно віддати контроль браузеру для плавності 60 FPS скролу або кліків), перериває узгодження.
  3. Пріоритезація: Важливі оновлення (ввід тексту в поле) мають вищий пріоритет над менш важливими (завантаження даних у списку у фоні).

Об’єкт Fiber (одиниця роботи) містить в собі інформацію про компонент, його стан, посилання на батьків, дітей та “братів” (Siblings).


6. Нові можливості React - автоматизація та продуктивність

З новими версіями React (починаючи з версії 18) механізми Virtual DOM стали ще розумнішими завдяки Concurrent Mode:


7. Практичні поради щодо оптимізації рендерингу

Розуміючи як працює Reconciliation, розробник повинен писати ефективніший код:

  1. Мемоїзація компонентів: React.memo (High-Order Component) порівнює пропси (props) поверхнево. Якщо пропси не змінилися, дочірній компонент не рендериться навіть тоді, коли рендериться батьківський.
  2. Уникнення інлайн-функцій у пропсах:

    // Погано: створює ВАЖКУ(нову) функцію при кожному рендері, ламаючи React.memo дочірніх компонентів.
    <button onClick={() => fetchData()}>Sync</button>
    
    // Добре: використання useCallback хука або винос функції назовні.
    
  3. Стабільні об’єкти: Навігація в useMemo для важких обчислень або об’єктів пропсів:
    // Зміна стилів або масиву генерує новий референс (посилання в пам'яті).
    // Погано: <List items={[]} />
    // Погано: <Component style= />
    

Висновки

  1. Virtual DOM не робить React “швидшим за нативний DOM”. Він дозволяє розробникам писати код так, ніби DOM можна нескінченно створювати з нуля, тоді як під капотом відбувається оптимальний патчинг ($O(n)$ замість $O(n^3)$).
  2. Алгоритм Reconciliation працює на упущеннях: компоненти одного типу створюють дерево, вузли різних типів ініціюють повну реконструкцію дерева.
  3. Атрибут key — це міст зв’язку між Virtual DOM елементами, який критичний для продуктивності списків. key повинен бути стабільним та унікальним для даних.
  4. React Fiber змінив парадигму з синхронного блокування браузера на пріоритезоване узгодження з паузами (Concurrent Mode).

Джерела

  1. Офіційна документація React. “Preserving and Resetting State”. URL: https://react.dev/learn/preserving-and-resetting-state
  2. React Docs. “Rendering Lists”. URL: https://react.dev/learn/rendering-lists
  3. Lin Clark. “A Cartoon Intro to Fiber”. React Conf 2017.
  4. “Reconciliation” - React Legacy Docs. URL: https://legacy.reactjs.org/docs/reconciliation.html
  5. “Virtual DOM and Internals”. URL: https://legacy.reactjs.org/docs/faq-internals.html
  6. React Core Team Discussions on Concurrent React.
  7. Dan Abramov’s Overreacted Blog: “React as a UI Runtime”.
  8. Acemarke. “A (Mostly) Complete Guide to React Rendering Behavior”. URL: https://blog.isquaredsoftware.com/
  9. “Understand the Virtual DOM”. Medium.
  10. “Why Web Developers Need to Care about the Event Loop” (Philip Roberts).
  11. “Understanding React Fiber architecture”. ITNext.
  12. ECMAScript 2025 Language Specification.
  13. “Performance optimization in React”. LogRocket Blog.

Запитання для самоперевірки

  1. Звідки береться проблема продуктивності при маніпуляціях “реальним DOM” об’єктом у браузері?
  2. Опишіть суть двох базових евристик (припущень), завдяки яким алгоритм Reconciliation досягає складності $O(n)$.
  3. Чому React прирівнює створення компонентів різних типів до необхідності “знищити і перебудувати все всередині (unmount / mount)”?
  4. Надайте технічні обґрунтування: чому використання індексів масиву як властивостей key може призвести до збоїв у стані інтерфейсу (наприклад, стан полів вводу зсувається)?
  5. Яку головну проблему багатокомпонентних додатків вирішила архітектура React Fiber у порівнянні з попереднім Stack-алгоритмом?