Пошук уроків, статей та іншого контенту
Розбираємось, як React використовує Virtual DOM для ефективного оновлення інтерфейсу.
Реальний DOM повільний для частих оновлень: кожна зміна може спричиняти перерахунок стилів (reflow) і перемальовування (repaint). Якщо оновлювати DOM напряму на кожну зміну стану — наприклад, у циклі, що оновлює текст сотні елементів списку по черзі — браузер може перераховувати layout сотні разів поспіль замість одного разу, і інтерфейс великих застосунків почне помітно гальмувати.
React (і схожі бібліотеки — Vue у режимі Virtual DOM, Preact) вирішують це не забороняючи прямі маніпуляції з DOM, а пропонуючи інший ментальний підхід: розробник описує, як має виглядати інтерфейс для поточного стану, а не пише покрокові інструкції, як саме перевести DOM зі старого вигляду в новий. Другу частину бере на себе бібліотека — і робить це саме через Virtual DOM.
Virtual DOM — це легка JavaScript-репрезентація реального DOM у вигляді дерева звичайних об'єктів (жодного зв'язку з браузерним API, тому створювати й порівнювати такі об'єкти на порядки дешевше, ніж чіпати справжній DOM). React будує нове віртуальне дерево при кожній зміні стану, порівнює його з попереднім деревом (процес називається diffing, або reconciliation) і застосовує до реального DOM лише мінімальний набір змін — так званий patch.
// Спрощена ідея diffing:
// 1. Побудувати нове віртуальне дерево
// 2. Порівняти з попереднім деревом, вузол за вузлом
// 3. Обчислити мінімальний набір змін (patch)
// 4. Застосувати patch до реального DOM однією "пачкою"Virtual DOM — не панацея для швидкості сам по собі, а компроміс: він спрощує ментальну модель («опиши, як має виглядати UI, і не думай про покрокові DOM-операції») ціною невеликих накладних витрат на побудову й порівняння дерев об'єктів при кожному рендері.
Алгоритм порівняння працює за практичними евристиками, а не ідеальним математичним порівнянням дерев (яке було б надто повільним — O(n³) для довільних дерев). React порівнює вузли на одному рівні дерева й приймає прості рішення:
Якщо тип елемента змінився (наприклад, <button> став <a>) — старе піддерево видаляється повністю, нове будується з нуля, без спроб «переробити» старе.
Якщо тип елемента той самий (<button> лишився <button>) — React лишає відповідний DOM-вузол на місці й оновлює лише ті атрибути/властивості, які реально змінились (наприклад, лише className, якщо змінився тільки він).
Для списків дочірніх елементів React використовує спеціальний ідентифікатор — key — щоб зрозуміти, які елементи лишились тими самими (просто змінили позицію), які нові, а які видалені.
Без key React порівнює елементи списку позиційно — за індексом. Якщо на початок масиву додати новий елемент, React побачить, що елемент на позиції 0 «змінився» (був старий перший елемент — став новий), елемент на позиції 1 теж «змінився» і так далі для всього списку — хоча насправді змінилась лише одна позиція, а решта елементів просто зсунулись.
// Погано: index як key — React не розрізняє "елемент зсунувся" від "елемент змінився"
{items.map((item, index) => (
<TodoItem key={index} text={item.text} />
))}
// Добре: стабільний унікальний ідентифікатор самого елемента даних
{items.map((item) => (
<TodoItem key={item.id} text={item.text} />
))}Зі стабільним key (наприклад, id із бази даних) React точно знає: «цей DOM-вузол відповідає саме цьому елементу даних, незалежно від його поточної позиції» — і може перевикористати вже наявний DOM-вузол (зі станом фокусу, введеним текстом у полі тощо) замість того, щоб перестворювати його.
Virtual DOM зменшує кількість реальних DOM-операцій, але сам процес побудови й порівняння віртуальних дерев — теж робота, яка займає час і зростає разом із розміром дерева. Для списку з десятками тисяч елементів навіть diffing без жодної реальної DOM-зміни може бути помітно повільним — тут потрібна вже інша техніка, віртуалізація списків (рендерити в DOM лише видимі на екрані елементи), яка Virtual DOM не замінює й не робить непотрібною.
Саме тому в останні роки набувають популярності фреймворки на основі сигналів (Solid.js, значною мірою Svelte) — вони взагалі відмовляються від Virtual DOM і diffing, компілюючи компонент напряму у покрокові DOM-оновлення для кожної конкретної реактивної змінної. Це не означає, що Virtual DOM — застарілий підхід: це різні компроміси між простотою ментальної моделі (React) і мінімізацією рантайм-накладних витрат (Solid).
Використання index масиву як key у списках, що можуть змінювати порядок, додавати чи видаляти елементи — призводить до втрати стану полів вводу, неправильних анімацій чи взагалі показу даних не того елемента.
Пряма мутація стану (наприклад, push у масив зі стану замість створення нового масиву) — React порівнює посилання на об'єкт/масив для оптимізації, і якщо посилання не змінилось, компонент може взагалі не перерендеритись, навіть якщо вміст усередині фактично інший.
Очікування, що Virtual DOM автоматично робить застосунок швидким — сам React усе одно виконує компонентні функції й diffing на кожен зайвий рендер; для справді великих списків чи важких обчислень потрібні додаткові інструменти (React.memo, useMemo, віртуалізація), а не сподівання на Virtual DOM.
Virtual DOM — легке дерево об'єктів, яке React будує при кожній зміні стану, порівнює з попереднім деревом (diffing/reconciliation) і застосовує до реального DOM лише мінімальний набір змін. Це спрощує розробку (не потрібно вручну керувати DOM-операціями), але не робить продуктивність автоматичною: стабільні key у списках, уникнення прямих мутацій стану та розуміння того, коли компоненти перерендерюються, — усе ще на відповідальності розробника.