Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Віртуальний DOM у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Віртуальний DOM** - це JavaScript-представлення реального DOM у пам'яті, яке React використовує для обчислення мінімальних змін інтерфейсу. ```jsx setCount(count + 1); // React будує новий Virtual DOM, виконує diff, патчить лише текстовий вузол з числом ``` **Ключове:** React порівнює два JS-дерева і застосовує лише різницю до реального DOM, не торкаючись незмінених вузлів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Віртуальний DOM** - це JavaScript-представлення реального DOM у пам'яті, яке React використовує для обчислення мінімальних змін інтерфейсу через diffing і точкові мутації реального DOM. ## Теорія ### TL;DR - Аналогія: спочатку ескіз на папері (Virtual DOM), потім фарба на стіну (реальний DOM) - наносиш рівно ті мазки, що потрібні - Головна різниця: прямі виклики DOM (`document.getElementById`) запускають синхронний reflow браузера; Virtual DOM спочатку збирає зміни і виконує diff - Правило вибору: часті оновлення стану → Virtual DOM; статичні сторінки без інтерактивності → звичайний HTML швидший - Ключі (`key`) у списках дозволяють React переставляти DOM-вузли замість перемонтування всіх підряд - Fiber у React 18 робить рендеринг переривчастим: взаємодія з користувачем має пріоритет над фоновими оновленнями ### Швидкий приклад ```jsx // Лічильник: React оновлює лише текстовий вузол, не весь div import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <h1>Count: {count}</h1> {/* Змінюється тільки цей текстовий вузол */} <button onClick={() => setCount(count + 1)}>+1</button> </div> ); } // При натисканні: React будує новий Virtual DOM, // порівнює зі старим, знаходить різницю лише в тексті <h1>, // і оновлює тільки цей один вузол у реальному DOM. ``` При кожному кліку React будує новий JavaScript-об'єкт, що описує UI, порівнює його з попереднім і передає браузеру тільки дельту. `<button>` і `<div>` у реальному DOM навіть не торкаються. ### Як працює алгоритм diffing Алгоритм diff у React виконується за O(n) часу з двома допущеннями. Перше: якщо типи елементів різні (наприклад, `<div>` став `<span>`), React знищує старе піддерево і будує нове з нуля. Жодних спроб узгодити. Друге: в списках пропс `key` дозволяє відстежити, які елементи перемістились, які додались, а які зникли. Без `key` React порівнює за індексом і перемонтовує все при переупорядкуванні. Reconciler (пакет `react-reconciler`) будує fiber-дерево: зв'язний список, де кожен вузол - це компонент. У React 18 ця робота переривчаста. Пріоритетне оновлення (наприклад, натискання клавіші) може перервати повільний рендер таблиці даних прямо посередині. ### Процес оновлення 1. **Тригер** - `setState` або `useState` спрацьовує, React ставить рендер у чергу 2. **Фаза рендеру** - React викликає компоненти і будує новий Virtual DOM у пам'яті (прості JS-об'єкти з `React.createElement`) 3. **Фаза commit** - React порівнює нове дерево з попереднім fiber-деревом і застосовує мінімальний набір мутацій через `commitRoot` Батчинг тут важливий. React групує кілька оновлень стану в один commit замість того, щоб flush після кожного виклику. У React 18 батчинг відбувається автоматично навіть усередині `setTimeout` або нативних обробників подій. ### Коли використовувати - Часті оновлення стану (введення даних користувача, дані в реальному часі) - Virtual DOM автоматично збирає їх у пакети - Великі динамічні списки - додай `key` пропси, а при понад 500 елементів підключи `react-window` - Налаштування продуктивності - поєднуй з `React.memo` або `useMemo` поверх Virtual DOM diffing - Статичні HTML-сторінки без інтерактивності - React тут надлишковий, прямий DOM або звичайний HTML швидші ### Типові помилки **Індекс масиву як `key`** ```jsx // Неправильно: при вставці на початок усі індекси зсуваються, React перемонтовує все items.map((item, i) => <li key={i}>{item.text}</li>) // Правильно: стабільний унікальний ідентифікатор items.map(item => <li key={item.id}>{item.text}</li>) ``` Індексні ключі нормально працюють для статичних списків, які ніколи не переупорядковуються. Але щойно з'являється сортування, фільтрація або вставка на початок, всі ключі зсуваються і React перемонтовує кожен вузол, втрачаючи локальний стан, фокус і поточні анімації. **Припущення, що Virtual DOM завжди швидший за прямий DOM** Для простого перемикання або заміни тексту `document.getElementById` буде швидшим. Накладні витрати React - це сам diff. За бенчмарками SyntheticJS, React приблизно у 4 рази повільніший за прямий DOM для тривіального оновлення одного елемента і у 10 разів швидший для складних списків, де батчинг запобігає сотням reflow. **Мутація стану напряму** ```jsx // Неправильно: diffing Virtual DOM спирається на порівняння посилань state.items.push(newItem); setState(state); // React бачить те саме посилання, може пропустити рендер // Правильно: нове посилання setState({ ...state, items: [...state.items, newItem] }); ``` **Глибокі дерева без віртуалізації** 10 000 рядків таблиці одночасно навантажать diff незалежно від Virtual DOM. Для таких випадків є `react-window` або `react-virtual` - вони рендерять лише видимі рядки. **Рендери від батька до дочірніх компонентів** Якщо батьківський компонент рендериться, всі його дочірні компоненти рендеряться теж, якщо не обгорнути їх у `React.memo`. Virtual DOM робить це дешевим, але не безкоштовним. React DevTools Profiler покаже, де реальні витрати. ### Де використовується в реальних проєктах - React core - кожен компонент рендериться у Virtual DOM-об'єкти через `jsx-runtime` перед будь-яким відображенням у браузері - Next.js - сервер рендерить Virtual DOM, потім гідратує його на клієнті, зіставляючи з реальним DOM - Preact - 3kb-клон Virtual DOM, використовується в дашборді Hulu і на 100 тис.+ сайтах, де важливий розмір бандлу - Redux DevTools - серіалізує снепшоти Virtual DOM для налагодження з відмоткою часу (time-travel debugging) - React Native - той самий механізм diffing Virtual DOM, але commit застосовується до нативних компонентів, а не до браузерного DOM ### Питання зі співбесід **Q:** Як React вирішує, замінювати піддерево чи оновлювати? **A:** Порівнює тип елемента. Якщо `<Input>` став `<Select>`, React демонтує `Input` і монтує `Select` з нуля. Якщо тип не змінився, патчить тільки змінені пропси. **Q:** Що відбувається з дублікатами або відсутніми ключами? **A:** Відсутні ключі замінюються індексом з попередженням у консолі. Дублікати призводять до некоректного diffing - React вибирає один елемент довільно і може показати неправильний UI без жодної помилки. **Q:** Як concurrent mode у React 18 змінює цикл Virtual DOM? **A:** `startTransition` позначає оновлення як низькопріоритетні. Fiber може перервати роботу над новим деревом посередині, дати пріоритетному оновленню (наприклад, натисканню клавіші) завершитись першим, а потім продовжити або відкинути перерване. Структура Virtual DOM та сама, але планувальник вирішує, коли відбудеться commit. **Q:** Навіщо `useMemo`, якщо вже є Virtual DOM? **A:** Коли компонент рендериться часто і в ньому є дочірні компоненти з важкими обчисленнями або великим піддеревом. `useMemo` повертає збережений результат, diff отримує те саме посилання і пропускає це піддерево повністю. **Q:** Чому не писати ручний diff замість React? **A:** Можна. Але React покриває 99% типових випадків без додаткового коду. Ручний diffing швидко перетворюється на технічний борг, особливо коли до проєкту додаються списки, анімації і конкурентні оновлення. ## Приклади ### Базовий: лічильник і мінімальні оновлення DOM ```jsx import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <h1>Count: {count}</h1> <button onClick={() => setCount(count + 1)}>+1</button> </div> ); } ``` Клікни три рази. React будує новий Virtual DOM при кожному кліку, порівнює з попереднім і патчить тільки текстовий вузол усередині `<h1>`. `<div>` і `<button>` у реальному DOM не змінюються. Відкрий React DevTools Profiler і побачиш точно, які компоненти перерендерились і скільки це зайняло. ### Середній: список задач із ключами ```jsx import { useState } from 'react'; const initial = [ { id: 1, text: 'Вивчити React', done: false }, { id: 2, text: 'Зібрати додаток', done: true }, ]; function TodoList() { const [todos] = useState(initial); const [filter, setFilter] = useState('all'); const visible = todos.filter(t => filter === 'all' ? true : t.done === (filter === 'done') ); return ( <> <button onClick={() => setFilter('all')}>Всі</button> <button onClick={() => setFilter('done')}>Виконані</button> <ul> {visible.map(todo => ( <li key={todo.id}>{todo.text}</li> // стабільний key = повторне використання DOM-вузла ))} </ul> </> ); } ``` При перемиканні фільтра React порівнює список, повторно використовує незмінені вузли `<li>` і додає або видаляє тільки те, що дійсно змінилось. Заміни `key={todo.id}` на `key={index}` і перевір у Profiler - побачиш, що кожен `<li>` перемонтовується при кожному перемиканні фільтра. ### Просунутий: індексні ключі проти стабільних при перетягуванні ```jsx // Погано: індексні ключі призводять до повного перемонтування при переупорядкуванні function BadList({ items }) { return ( <ul> {items.map((item, index) => ( <Item key={index} data={item} /> // перетягнути елемент → 100 розмонтувань + 100 монтувань ))} </ul> ); } // Добре: стабільні ключі-ID дозволяють React дешево міняти позиції function GoodList({ items }) { return ( <ul> {items.map(item => ( <Item key={item.id} data={item} /> // перетягнути → React міняє fiber-вузли місцями, стан зберігається ))} </ul> ); } ``` При 100 елементах з можливістю перетягування `BadList` запускає 100 розмонтувань і 100 монтувань при кожному переупорядкуванні, бо зсуваються всі індекси. `GoodList` лише міняє fiber-вузли місцями. У React Native `FlatList` ця різниця виявляється як видиме мерехтіння і втрата позиції прокрутки на Android. Я сам бачив цю помилку в PR від досвідчених розробників.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.