Пошук уроків, статей та іншого контенту
Розбираємось, як React використовує Virtual DOM для ефективного оновлення інтерфейсу.
Virtual DOM — це легковаге JavaScript-представлення структури інтерфейсу, яке фреймворк або бібліотека зберігає в пам’яті.
У браузері інтерфейс представлений не лише HTML-текстом, а деревом об’єктів — DOM (Document Object Model). Коли JavaScript змінює це дерево, браузер може виконувати додаткову роботу:
перераховувати стилі;
змінювати геометрію елементів;
перемальовувати частини сторінки;
оновлювати візуальний результат.
Часті та невеликі зміни DOM можуть бути дорогими, особливо якщо вони спричиняють каскад інших обчислень.
Virtual DOM допомагає описувати бажаний стан інтерфейсу в JavaScript, а потім оновлювати реальний DOM лише там, де це необхідно.
React працює за декларативною моделлю. Розробник описує, яким має бути інтерфейс для певного стану, а не покроково вказує, які DOM-методи потрібно викликати.
Наприклад:
function Counter({ count }) {
return (
<button>
Лічильник: {count}
</button>
);
}Якщо значення count змінюється з 0 на 1, компонент повторно обчислює свою розмітку. React отримує нове дерево елементів і порівнює його з попереднім.
У спрощеному вигляді процес має такий вигляд:
Змінюється стан або вхідні властивості компонента.
React викликає компонент повторно.
Створюється нове Virtual DOM-дерево.
React порівнює нове дерево з попереднім.
Визначаються необхідні зміни.
Зміни застосовуються до реального DOM.
Процес порівняння дерев називають reconciliation — узгодженням або reconciliation-процесом.
Уявімо, що компонент спочатку повертає:
<h1>Привіт</h1>Після зміни стану він повертає:
<h1>Вітаємо!</h1>Структура елемента залишилася тією самою: це все ще h1. Змінився лише текст. React не обов’язково замінює весь елемент. Він може оновити тільки текстовий вузол.
Інший приклад:
function Status({ isOnline }) {
return (
<div className={isOnline ? 'online' : 'offline'}>
{isOnline ? 'У мережі' : 'Не в мережі'}
</div>
);
}Під час зміни isOnline React:
повторно обчислить результат компонента;
побачить, що div залишився на тому самому місці;
визначить зміну className;
оновить клас і текст.
Розробнику не потрібно вручну викликати querySelector, textContent або classList.
Virtual DOM не є повною копією браузерного DOM. Це структура даних, яка описує елементи інтерфейсу.
Спрощено елемент JSX:
<button className="primary">
Зберегти
</button>можна уявити як об’єкт на кшталт:
{
type: 'button',
props: {
className: 'primary',
children: 'Зберегти'
}
}Це лише концептуальне представлення. Внутрішні структури React складніші та можуть змінюватися між версіями.
Важливо розділяти:
реальний DOM — об’єкти, якими керує браузер;
Virtual DOM — внутрішнє представлення, яке використовує React;
JSX — синтаксис, що дає змогу зручно описувати елементи;
компонент — функція або клас, який формує частину інтерфейсу.
Без React інтерфейс можна оновлювати імперативно:
const button = document.querySelector('button');
const title = document.querySelector('h1');
button.addEventListener('click', () => {
title.textContent = 'Нове повідомлення';
});Цей підхід працює, але зі зростанням застосунку потрібно самостійно контролювати:
які елементи вже існують;
які дані відповідають цим елементам;
коли їх оновлювати;
що робити після додавання або видалення елементів;
як не залишити застарілі обробники подій;
як уникнути конфліктів між різними частинами коду.
React пропонує іншу модель:
function Message({ hasMessage }) {
return hasMessage
? <p>У вас є нове повідомлення</p>
: <p>Нових повідомлень немає</p>;
}Компонент описує результат для поточного стану. React бере на себе узгодження цього опису з DOM.
Це не означає, що прямі зміни DOM ніколи не потрібні. Вони можуть використовуватися для інтеграції зі сторонніми бібліотеками, роботи з фокусом, вимірювання елементів або інших спеціальних випадків. Проте основний інтерфейс зазвичай краще залишати під контролем React.
Теоретично порівняння двох довільних дерев може бути складним і вимагати значних обчислень. React використовує практичні припущення, які дають змогу виконувати цей процес ефективніше.
Якщо тип елемента змінився, React зазвичай розглядає це як заміну піддерева.
// Попередній результат
<div>Вміст</div>
// Новий результат
<section>Вміст</section>У такому випадку div і його піддерево можуть бути видалені, а замість них створено section.
Якщо тип однаковий, React порівнює властивості та дочірні елементи:
// Попередній результат
<button className="secondary">Надіслати</button>
// Новий результат
<button className="primary">Надіслати</button>Тут сам button може залишитися тим самим DOM-елементом, а React оновить лише клас.
Розглянемо список:
<ul>
<li>Апельсин</li>
<li>Яблуко</li>
</ul>Якщо на початок додати новий елемент:
<ul>
<li>Банан</li>
<li>Апельсин</li>
<li>Яблуко</li>
</ul>Без додаткової інформації React може орієнтуватися на позиції елементів. Тоді перший li буде оновлено з «Апельсин» на «Банан», другий — з «Яблуко» на «Апельсин», а третій буде створено.
Для списків це може призвести до зайвих оновлень і проблем зі збереженням стану дочірніх компонентів. Саме тому використовують ключі.
keykey — це стабільний ідентифікатор елемента списку. Він допомагає React зрозуміти, який елемент залишився тим самим, який був доданий, а який видалений.
function ProductList({ products }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name}
</li>
))}
</ul>
);
}Якщо порядок товарів зміниться, React зможе зіставити елементи за product.id, а не лише за їхньою позицією.
Хороший ключ має бути:
унікальним у межах поточного списку;
стабільним між повторними рендерами;
пов’язаним із сутністю елемента.
Не варто без потреби використовувати індекс масиву:
{products.map((product, index) => (
<li key={index}>{product.name}</li>
))}Індекс може бути прийнятним для статичного списку, який ніколи не сортується, не видаляється та не змінюється. Для динамічних списків краще використовувати ідентифікатор самого елемента.
У сучасному React оновлення умовно можна поділити на дві фази.
React викликає компоненти та визначає, яким має бути нове дерево. На цьому етапі він порівнює попередній і новий результат та планує зміни.
Цю фазу часто називають render phase. Вона не означає обов’язкове негайне оновлення браузерного DOM.
Після визначення змін React застосовує їх до реального DOM. Цю фазу називають commit phase.
Наприклад, React може:
створити новий DOM-елемент;
видалити непотрібний елемент;
змінити атрибут або властивість;
оновити текст;
додати чи прибрати обробник події.
У React Fiber внутрішня реалізація дає змогу краще планувати роботу з деревом компонентів. Однак це не означає, що кожен рендер одразу змінює DOM або що Virtual DOM сам по собі є окремим потоком виконання.
Популярне спрощення звучить так: «Virtual DOM робить React швидшим за прямі зміни DOM». Це не зовсім правильно.
Virtual DOM додає власну роботу:
потрібно створити нове представлення;
порівняти його з попереднім;
визначити зміни;
застосувати їх до DOM.
Якщо застосунок дуже простий і змінюється лише один елемент, ручне оновлення DOM може бути швидшим за повний цикл бібліотеки.
Переваги Virtual DOM полягають не тільки в абсолютній швидкості. Він допомагає:
організувати складний інтерфейс;
централізовано керувати оновленнями;
зменшити кількість ручних DOM-операцій;
зробити код декларативним;
передбачувано синхронізувати стан і представлення;
будувати компоненти, які простіше перевикористовувати та тестувати.
Тому коректніше казати, що React намагається ефективно застосовувати зміни до DOM, а Virtual DOM є частиною механізму, який це забезпечує.
Компонент може повторно обчислитися, коли:
змінився його стан;
змінилися його props;
повторно обчислився батьківський компонент;
відбулося оновлення контексту, який він використовує.
Повторний рендер компонента не обов’язково означає повну перебудову реального DOM. React порівнює результат і застосовує лише потрібні зміни.
Наприклад, у цьому компоненті оновлення лічильника може не змінити частину інтерфейсу, яка залишилася такою самою:
function Dashboard({ user }) {
const [count, setCount] = React.useState(0);
return (
<main>
<h1>Профіль: {user.name}</h1>
<button onClick={() => setCount(count + 1)}>
Значення: {count}
</button>
</main>
);
}Коли змінюється count, компонент обчислюється повторно. Але h1 не потребує змін, якщо user.name залишився тим самим.
Virtual DOM не:
прискорює будь-який JavaScript-код;
усуває всі проблеми продуктивності;
забороняє прямий доступ до DOM;
автоматично робить компоненти добре спроєктованими;
замінює оптимізацію великих списків, зображень або мережевих запитів;
гарантує, що кожен компонент оновиться лише один раз.
Продуктивність залежить також від:
структури компонентів;
розміру та складності дерева;
кількості елементів у списках;
частоти оновлень;
використання стабільних ключів;
обсягу виконуваної логіки під час рендера;
роботи браузера зі стилями та макетом.
Для дуже великих списків можуть знадобитися окремі підходи, наприклад віртуалізація списку. Це інше поняття: Virtual DOM описує представлення інтерфейсу, а virtualization обмежує кількість елементів списку, які реально відображаються.
React повторно обчислює компоненти відповідно до правил оновлення. Потім reconciliation визначає, які частини DOM справді потрібно змінити.
Повторний виклик функції компонента та зміна реального DOM — це різні процеси.
Такий варіант погіршує зіставлення елементів:
{items.map((item) => (
<li key={Math.random()}>
{item.name}
</li>
))}Під час кожного рендера ключі будуть новими. React може сприймати елементи як повністю нові, через що втрачатиметься їхній локальний стан і виконуватиметься зайва робота.
Якщо елементи додаються, видаляються або переміщуються, індекс перестає бути стабільним ідентифікатором. Це може спричинити неправильне зіставлення стану та DOM-елементів.
key доступний через propskey потрібен React для внутрішнього зіставлення елементів і не передається компоненту як звичайна властивість.
Якщо компоненту потрібен ідентифікатор, передайте його окремо:
<ProductRow
key={product.id}
productId={product.id}
product={product}
/>Не кожен повторний рендер є проблемою. Перед використанням мемоізації або складних оптимізацій варто виміряти продуктивність і визначити реальне вузьке місце.
Спочатку краще:
зменшити непотрібні оновлення стану;
правильно організувати структуру компонентів;
використовувати стабільні ключі;
не виконувати важкі обчислення без потреби;
перевірити застосунок за допомогою інструментів профілювання.
Virtual DOM — це внутрішнє представлення інтерфейсу в пам’яті, яке React використовує для узгодження стану компонентів із реальним DOM.
Основний процес такий:
компонент отримує новий стан або props;
React обчислює нове дерево елементів;
порівнює його з попереднім;
визначає мінімально необхідні зміни;
застосовує їх до реального DOM.
Найважливіше — Virtual DOM не є магічним прискорювачем. Його головна цінність у декларативній моделі та автоматичному керуванні змінами інтерфейсу. Розробник описує, що має бути показано, а React відповідає за узгодження цього опису з DOM.