Начальный 7 мин чтения

Virtual DOM

Virtual DOM — абстракция реального DOM в виде JS-объектов. React использует её для эффективного обновления интерфейса через алгоритм сравнения (reconciliation).

Virtual DOMreconciliationReactrendering

Проблема прямой работы с DOM

Операции с реальным DOM дорогостоящие — каждое изменение может вызвать reflow и repaint браузера:

// Наивный подход — 1000 операций с DOM
for (let i = 0; i < 1000; i++) {
  document.getElementById('list').innerHTML += `<li>${i}</li>`
  // Каждая итерация вызывает reflow!
}

// Лучше — одна операция
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = i
  fragment.appendChild(li)
}
document.getElementById('list').appendChild(fragment)

Virtual DOM автоматизирует эту оптимизацию.

Что такое Virtual DOM?

Virtual DOM (VDOM) — это лёгкое JavaScript-представление реального DOM в виде дерева объектов.

// Реальный DOM:
// <div class="card"><h2>Заголовок</h2><p>Текст</p></div>

// Virtual DOM:
{
  type: 'div',
  props: { className: 'card' },
  children: [
    {
      type: 'h2',
      props: {},
      children: ['Заголовок']
    },
    {
      type: 'p',
      props: {},
      children: ['Текст']
    }
  ]
}

JSX компилируется в такие объекты через React.createElement().

Процесс обновления

1. Состояние изменилось (setState / useState)
       ↓
2. Компонент рендерится → создаётся новый VDOM
       ↓
3. Алгоритм Diffing сравнивает старый и новый VDOM
       ↓
4. Вычисляются минимальные изменения (patch)
       ↓
5. Только изменённые узлы обновляются в реальном DOM

Алгоритм Reconciliation

React использует эвристики для O(n) сравнения деревьев (вместо теоретического O(n³)):

Правило 1: Разные типы = пересоздание

// До
<div><Counter /></div>

// После — тип изменился с div на span
<span><Counter /></span>
// Counter будет размонтирован и создан заново!

Правило 2: Одинаковые типы = обновление атрибутов

// До
<button className="btn" disabled={false} />

// После — React только обновит атрибуты
<button className="btn btn--active" disabled={true} />

Правило 3: key для списков

// Без key — React сравнивает по позиции
// Добавление элемента в начало заставит обновить все элементы
<ul>
  <li>A</li>
  <li>B</li>
</ul>

// С key — React отслеживает по идентификатору
// Добавление элемента = одна вставка
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>

Пакетирование обновлений

React 18 автоматически группирует обновления состояния:

function handleClick() {
  setCount(c => c + 1)   // Не вызывает рендер сразу
  setName('Иван')         // Не вызывает рендер сразу
  setActive(true)         // Не вызывает рендер сразу
  // Один ре-рендер после всех трёх обновлений
}

React.memo

Предотвращает лишние ре-рендеры дочернего компонента:

import { memo } from 'react'

const UserCard = memo(function UserCard({ name, age }) {
  console.log('Рендер UserCard')
  return <div>{name}, {age}</div>
})

// Теперь UserCard рендерится только при изменении name или age
function Parent() {
  const [count, setCount] = useState(0)
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <UserCard name="Иван" age={25} /> {/* Не ре-рендерится при клике */}
    </>
  )
}

Fiber — новый движок

React 16 представил Fiber — полная переработка алгоритма reconciliation:

  • Приоритеты — срочные обновления (ввод, клики) обрабатываются раньше
  • Прерываемость — тяжёлая работа может быть прервана и возобновлена
  • Concurrent Mode — рендеринг без блокировки UI
// React 18 — Concurrent Features
import { startTransition, useDeferredValue } from 'react'

function Search() {
  const [query, setQuery] = useState('')
  const deferredQuery = useDeferredValue(query)
  // Ввод отзывчивый, фильтрация — некритичная задача
  const results = useMemo(
    () => filterItems(deferredQuery),
    [deferredQuery]
  )

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ResultsList items={results} />
    </>
  )
}

Virtual DOM vs. Сигналы

Современные фреймворки (SolidJS, Vue 3 Signals, Svelte) уходят от Virtual DOM в пользу реактивных сигналов — прямого обновления только изменившихся узлов без диффинга. Это потенциально более эффективно для гранулярных обновлений.