Головна / Статті / Як браузер малює елементи та яке місце посідає React у цьому процесі

Як браузер малює елементи та яке місце посідає React у цьому процесі

Дізнайтеся, як Critical Rendering Path, reconciliation, Fiber та Scheduler працюють разом, щоб перетворювати оновлення React на пікселі на екрані.

1798 слів

По-перше, забудьте про React: як насправді браузер малює сторінку?

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

HTML → DOM tree
CSS  → CSSOM tree
DOM + CSSOM → Render Tree
Render Tree → Layout → Paint → Composite → Screen

Кожен етап виконує певну функцію, і назви самі по собі цього не розкривають:

  • Дерево DOM — браузер парсить ваш HTML та перетворює його на дерево вузлів. Це суто структурний елемент: які елементи знаходяться всередині яких.
  • Дерево CSSOM — та сама логіка застосовується до стилів. Кожне правило CSS, яке ви написали, перетворюється на дерево, яке браузер може використовувати для пошуку інформації.
  • Дерево відображення — браузер поєднує два дерева: він пройшовся по DOM, приєднав відповідні правила CSSOM до кожного вузла та проігнорував усе, що насправді не буде видимим (елемент із display: none все ще залишається в DOM, але не входить до дерева відображення).
  • Розташування — це етап обчислень. Враховуючи кожен елемент та його обчислені стилі, браузер визначає точне положення та розмір кожного з них.
  • Малювання — тепер браузер заповнює візуальний контент: текст, кольори фону, рамки, тіні тощо.
  • Композиція — коли на сторінці є кілька шарів (що часто трапляється з міркувань продуктивності), браузер викладає їх у правильному порядку, і саме цей кінцевий результат потрапляє на ваш екран.
  • Разом ці кроки утворюють те, що називається Критичним шляхом відображення, або CRP.

    Ця послідовність виконується однаково незалежно від того, чи ви використовуєте React, Vue, звичайний jQuery чи взагалі жодної бібліотеки. Малювання пікселів — це обов’язок браузера, а не функція будь-якої фреймворк-системи користувацького інтерфейсу.

    То яке місце займає React у всьому цьому?

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

    Без React оновлення інтерфейсу означає вручну знаходити відповідний елемент DOM та самостійно його змінювати:

    const counterEl = document.getElementById('counter');
    counterEl.textContent = newCount;
    

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

    З React та саме оновлення виглядає ось так:

    function Counter({ count }) {
      return <p>{count}</p>;
    }
    

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

    State changes → React figures out what changed → applies a small patch to the real DOM
                                                                  ↓
                              browser does its normal thing: Layout → Paint → Composite → Screen
    

    Уся сутність React полягає у тому, щоб зробити цей перший крок — визначення того, що змінилося, — якомога швидше та точніше, щоб браузер мусив перевиконувати операції Layout та Paint лише для тієї частини сторінки, якій це дійсно потрібно, а не для всього контенту.

    Імперативний vs декларативний підхід: зміна способу мислення

    Цей контраст пояснює, чому React був спроектований саме так.

    Імперативний код описує кожен окремий крок:

    list.innerHTML = '';
    for (const item of items) {
      const li = document.createElement('li');
      li.textContent = item;
      list.appendChild(li);
    }
    

    Це ви вирішуєте: стерти це, створити той елемент, приєднати його сюди.

    Декларативний код натомість описує бажаний результат:

    <ul>
      {items.map(item => <li key={item}>{item}</li>)}
    </ul>
    

    Замість того, щоб наказувати браузеру „створити li та вставити його“, ви формулюєте: „з урахуванням цього масива, ось яким має бути кінцевий інтерфейс“. Щось інше має перетворити цей опис на конкретні операції з DOM — і наступним кроком є розуміння того, що саме це є.

    Що насправді означає „узгодження“

    Цей аспект здається складнішим, ніж він є насправді, якщо подивитися на нього безпосередньо.

    React зберігає у своїй пам’яті „знімок“ вашого інтерфейсу, який називається Віртуальним DOM. Кожного разу, коли щось змінюється, React створює нову версію цього дерева та порівнює її з попередньою, щоб визначити, що змінилося. Саме цей крок порівняння називають узгодженням.

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

    1. Тип елемента змінився (наприклад, `

    ` перетворився на ``) — React навіть не дивиться на дочірні елементи; він повністю видаляє старий вузол та створює новий.

    1. Тип елемента залишився незмінним (`

    стає

    `) — React зберігає існуючий реальний DOM-вузол та лише виправляє ті частини, які змінилися.

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

    Ось чому завжди слід присвоювати стабільний key елементам списку:

    {items.map(item => <li key={item.id}>{item.name}</li>)}
    

    Як тільки елементи мають ключ, React може розпізнати, що „цей конкретний елемент лише змінив позицію“, замість того щоб припускати, що весь список було перебудовано.

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

    Хіба це не те саме, що робить Fiber?

    Це цілком закономірне запитання, яке варто розглянути уважно.

    Основною ідеєю є примирення даних, а Fiber — це просто інструмент, який його реалізує.

    До версії React 16 цей інструмент називався Stack Reconciler. Він рекурсивно та синхронно обробляв усю ієрархію даних, тож як тільки починав свою роботу, мусив завершитися повністю, перш ніж зупинитися. При великих оновленнях це могло заблокувати основну смугу виконання на достатньо довгий час, через що додаток починав працювати повільно — кадри могли зникати, а введення тексту ставало нереактивним.

    Fiber, який був введений у React 16, замінив цю систему. Концепція примирення не змінилась, але тепер робота ділиться на невеликі частини, які можна призупинити, видалити або продовжити пізніше. Якщо з’являється щось більш термінове — наприклад, введення тексту користувачем — React може перервати будь-яку роботу нижчого пріоритету, обробити цю термінову зміну, а потім повернутися до того місця, де зупинився.

    Тож некоректно казати, що Fiber замінив примирення. Більш точно буде сказати, що старий двигун, який виконував примирення, було замінено на більш потужний.

    То що робить Scheduler?

    Fiber робить можливим призупинення та відновлення роботи, але щось інше має вирішувати, коли призупинити та яке завдання має пріоритет. Саме це і є завданням Scheduler.

    Його обов’язки включають:

    • Оновлення рангів за пріоритетом терміновості — наприклад, вважати надсилання символа у поле введення терміновим, тоді як оновлення далекого фонового списку — ні.
    • Заповнення проміжків між фреймами браузера для поступового виконання завдань нижчої пріоритетності та відступу перед необхідністю відображення наступного фрейма.
    • Увімкнення можливостей React 18, таких як startTransition, де позначення оновлення як нетермінового фактично повідомляє планувальник про те, що можна відкласти цю роботу на нижчу позицію у списку пріоритетів.

    Проста ментальна модель поєднує ці три концепції:

    Reconciliation  → the algorithm (what changed?)
    Fiber           → the engine that makes that algorithm interruptible
    Scheduler       → the traffic controller deciding when to pause/resume Fiber's work
    

    Етап відображення проти етапу збереження

    Є ще одна відмінність, яку варто зрозуміти: Fiber ділить свою роботу на два етапи, які діють за зовсім різними правилами.

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

    Етап збереження змін — це момент, коли React нарешті записує зміни у справжній DOM та застосовує їх. Цей етап не можна перервати — він виконується повністю за один безперервний прохід, оскільки часткове оновлення інтерфейсу залишило б сторінку у пошкодженому візуальному стані. Відразу після оновлення DOM, але до того, як браузер намалює екран, синхронно викликається useLayoutEffect. На відміну від нього, useEffect виконується трохи пізніше, після того, як браузер вже завершить малювання.

    Чи справді вам потрібен React?

    Чесно кажучи, не завжди. Багато веб-сайтів у продакшені працюють лише за допомогою HTML, CSS та звичайного JavaScript, і вони функціонують чудово.

    React починає виправдовувати свою додаткову складність, коли ваші вимоги стають більш складними:

    • Ручна налаштування оновлень DOM є прийнятною для невеликих проектів, але стає некерованою, коли потрібно керувати десятками взаємопов’язаних елементів інтерфейсу.
    • Значна кількість помилок у реальних інтерфейсах виникає через те, що стан та відображуваний інтерфейс втрачають синхронізацію. Підхід React — розглядати інтерфейс як функцію від стану та дозволяти фреймворку керувати відмінностями — за своєю суттю усуває більшу частину цього ризику.
    • Можливість створювати повторно використовувані компоненти, підтримувані екосистемою інструментів маршрутизації, інструментів розробки та спільних стандартів, стає цінною, коли більше одного розробника працює над одним і тим самим кодом.

    Однак для простої сторінки приземлення чи переважно статичного сайту звичайний JavaScript є кращим вибором. Використання Fiber, Scheduler та повного механізму узгодження означатиме необхідність оплати додаткових витрат за проблему, якої насправді немає.

    React не є за своєю суттю кращим за JavaScript. Це набір інструментів, створений для вирішення однієї конкретної проблеми: підтримки синхронізації користувацького інтерфейсу зі станом, який постійно змінюється у великих масштабах серед команди. При менших обсягах роботу звичайний HTML, CSS та JS виконують її ідеально.

    Пов’язана література

  • Як уникнути проблем зі станом системи, спричинених змінами посилань у JavaScript — Дізнайтеся, чому зміна об’єктів та масивів через посилання порушує процес перерендерингу в React, чому функція spread копіює дані лише поверхнево, та як безпечно створювати глибокі клони стану.
  • Практичне порівняння шаблонів структури папок у React — Пояснюється структура проектів React, заснована на функціях, шарах та домені, а також надаються рекомендації щодо вибору найкращого підходу у міру зростання вашого додатку.