React — виртуальный DOM и reconciliation

Virtual DOM (VDOM) — JavaScript-представление дерева DOM в памяти. Reconciliation — алгоритм React, который сравнивает новый VDOM со старым (diffing) и вычисляет минимальный набор реальных DOM-операций для обновления UI.

Зачем нужно

Прямые манипуляции с реальным DOM дорогостоящи — каждое изменение может вызвать reflow и repaint. React батчит обновления, находит только изменившиеся узлы и применяет минимум операций к реальному DOM. Понимание reconciliation помогает писать перформантный код и избегать ненужных ре-рендеров.

Где используется

  • Оптимизация компонентов с частыми обновлениями состояния
  • Понимание причин ненужных ре-рендеров (для использования React.memo, useMemo)
  • Правильное использование key prop в списках

Основной контент

Как работает reconciliation

1. Триггер: setState / useState setter / props change
2. Рендер: React вызывает функцию компонента → новый VDOM
3. Diffing: сравнение нового VDOM со старым (O(n), не O(n³))
4. Commit: применение изменений к реальному DOM

Алгоритм эвристики:
- Разные типы элементов → полное перестроение поддерева
- Одинаковый тип → обновление атрибутов
- Массивы → сравнение по key prop

Пример: почему key важен

// ПЛОХО — key по индексу (перемешивание → полный перерендер)
{items.map((item, index) => (
  <ListItem key={index} item={item} />
))}

// ХОРОШО — key по стабильному уникальному идентификатору
{items.map(item => (
  <ListItem key={item.id} item={item} />
))}

// Почему это важно:
// При удалении первого элемента с key=index:
// - React думает, что изменились ВСЕ последующие элементы
// - Перерендеривает всё дерево
// С key=item.id React корректно сопоставляет элементы

Батчинг обновлений (React 18)

// React 18: все обновления батчатся автоматически
function handleClick() {
  setCount(c => c + 1); // Не вызывает ре-рендер сразу
  setActive(true);      // Не вызывает ре-рендер сразу
  // Один ре-рендер с обоими обновлениями
}

// React 17 и ниже: батчинг только в event handlers
// Async обновления не батчились автоматически
setTimeout(() => {
  setCount(c => c + 1); // Ре-рендер!
  setActive(true);      // Ещё ре-рендер! (2 рендера вместо 1)
}, 0);

Fiber — архитектура React 16+

Fiber — структура данных для каждого компонента.
Позволяет прерывать и возобновлять reconciliation.

Две фазы:
  Render phase  — прерываемая, вычисляет изменения (без side effects)
  Commit phase  — синхронная, применяет изменения к DOM

Приоритеты (React Concurrent Mode):
  Срочные (urgent): клики, ввод текста → немедленно
  Несрочные (transition): поиск, фильтрация → могут прерваться
// startTransition — пометить обновление как несрочное
import { startTransition } from 'react';

function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState();

  function handleChange(e) {
    setQuery(e.target.value); // Срочное — UI должен отвечать сразу

    startTransition(() => {
      setResults(heavySearch(e.target.value)); // Несрочное — можно прервать
    });
  }

  return <input value={query} onChange={handleChange} />;
}

Частые ошибки

  • Использование индекса массива как key для изменяемых списков
  • Создание новых объектов/функций в render без useMemo/useCallback → ненужные ре-рендеры
  • Слишком глубокое дерево компонентов — ре-рендер наверху перерендеривает всё дерево
  • Изменение state в render (бесконечный цикл)

Связанные темы

Ресурсы