Как работает Virtual DOM в React

10 сентября 2026
Автор

Антон Ларичев

Что такое Virtual DOM

В браузере DOM (Document Object Model) — это дерево объектов, которое представляет HTML-страницу. Любое обновление DOM обходится дорого: браузер пересчитывает стили, выполняет layout и перерисовывает пиксели. Прямые манипуляции с DOM через document.querySelector или innerHTML могут вызывать сотни таких операций при каждом изменении.

Virtual DOM (VDOM) — это лёгкое JavaScript-представление реального DOM-дерева. React хранит его в памяти и при каждом изменении состояния сравнивает новое дерево со старым, находит минимальный набор изменений и применяет только их к реальному DOM.

По сути VDOM — это обычный JavaScript-объект:

// Реальный DOM-элемент
// <div class="card">
//   <h2>Привет</h2>
// </div>

// Его представление в Virtual DOM
{
  type: 'div',
  props: { className: 'card' },
  children: [
    {
      type: 'h2',
      props: {},
      children: ['Привет']
    }
  ]
}

Операции с таким объектом выполняются в тысячи раз быстрее, чем с реальным DOM, потому что не запускают механизмы браузера.

Курс по теме

Пройти курс React и Redux Toolkit

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Как React создаёт Virtual DOM

Каждый JSX-элемент компилируется в вызов React.createElement (или _jsx в современных сборках с новым JSX-трансформом):

// JSX
const element = (
  <div className="card">
    <h2>Привет</h2>
  </div>
);

// Что получается после компиляции
const element = React.createElement(
  'div',
  { className: 'card' },
  React.createElement('h2', null, 'Привет')
);

Результат — «React-элемент», простой иммутабельный объект:

{
  $$typeof: Symbol(react.element),
  type: 'div',
  props: {
    className: 'card',
    children: { type: 'h2', props: { children: 'Привет' } }
  },
  key: null,
  ref: null
}

Etот объект описывает то, что должно быть на экране. React хранит дерево таких объектов и сравнивает его с предыдущей версией при каждом рендере.

Алгоритм сравнения (Reconciliation и Diffing)

Процесс применения изменений из Virtual DOM в реальный называется reconciliation (сверка). Внутри него работает алгоритм diffing — поиск различий между двумя деревьями.

Наивное сравнение двух деревьев требует O(n³) операций, что недопустимо для реальных приложений. React использует два эвристических допущения, которые снижают сложность до O(n):

  1. Два элемента разного типа порождают разные деревья. Если корневой элемент изменил тип — React уничтожает старое поддерево и строит новое с нуля.
  2. Ключи (key) позволяют сопоставлять дочерние элементы между рендерами. С их помощью React понимает, какой элемент остался на месте, а какой переместился.

Правило 1: смена типа элемента

// До обновления
<div>
  <Counter />
</div>

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

React видит, что корневой тип изменился с div на span, уничтожает весь компонент Counter (вызывает componentWillUnmount / эффект очистки), и создаёт новый экземпляр.

Если тип остался прежним — React сравнивает атрибуты и обновляет только изменившиеся:

// До
<div className="old" title="подсказка" />

// После
<div className="new" title="подсказка" />

// React обновит только className, title трогать не будет

Правило 2: список дочерних элементов и ключи

Рассмотрим список без ключей:

// До
<ul>
  <li>Яблоко</li>
  <li>Банан</li>
</ul>

// После — добавили элемент в конец
<ul>
  <li>Яблоко</li>
  <li>Банан</li>
  <li>Вишня</li>
</ul>

React сравнивает позицию за позицией. Первые два совпали — добавлен только третий. Это эффективно.

Но если добавить элемент в начало без ключей:

// До
<ul>
  <li>Яблоко</li>
  <li>Банан</li>
</ul>

// После — добавили элемент в начало
<ul>
  <li>Вишня</li>
  <li>Яблоко</li>
  <li>Банан</li>
</ul>

React сравнит позицию 0: Яблоко vs Вишня — разные, обновить. Позицию 1: Банан vs Яблоко — разные, обновить. Позиция 2 — новый элемент. В итоге обновлены все три узла, хотя можно было добавить только один.

Ключи решают эту проблему:

<ul>
  {items.map(item => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>

Теперь React сопоставляет элементы по key, а не по позиции. Он видит, что Яблоко и Банан никуда не делись — просто сместились вниз. В DOM вставляется только один новый узел.

Важно: никогда не используйте индекс массива как ключ в списке, который может изменяться или переупорядочиваться:

// Плохо — индекс меняется при сортировке или удалении
{items.map((item, index) => (
  <li key={index}>{item.name}</li>
))}

// Хорошо — стабильный идентификатор
{items.map(item => (
  <li key={item.id}>{item.name}</li>
))}

React Fiber: современная реализация Virtual DOM

До React 16 алгоритм reconciliation работал синхронно: начав обход дерева, он не мог остановиться до завершения. При большом дереве это блокировало главный поток и вызывало пропуск кадров.

React 16 представил Fiber — переписанный движок reconciliation. Fiber разбивает работу на небольшие единицы (fiber-узлы) и умеет прерывать, откладывать и возобновлять их выполнение.

Каждый компонент получает соответствующий fiber-узел — объект, который хранит:

{
  type: MyComponent,       // тип компонента
  stateNode: instance,     // ссылка на DOM-узел или экземпляр класса
  child: fiberNode,        // первый дочерний узел
  sibling: fiberNode,      // следующий соседний узел
  return: fiberNode,       // родительский узел
  pendingProps: {},        // новые пропсы
  memoizedProps: {},       // применённые пропсы
  memoizedState: {},       // текущее состояние
  effectTag: 'UPDATE',     // что нужно сделать с DOM
}

Fiber работает в две фазы:

Фаза Render (Reconciliation)

React строит новое fiber-дерево («work-in-progress tree»), сравнивая его с текущим. Эта фаза прерываемая: браузер может взять управление для обработки ввода или отрисовки кадра, а React потом продолжит.

Фаза Commit

Когда сравнение завершено, React применяет все накопленные изменения к реальному DOM за один синхронный проход. Эта фаза не прерывается — иначе пользователь увидел бы частично обновлённый интерфейс.

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Счётчик: {count}</p>
      {/* React обновит только текстовый узел внутри <p> */}
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

При клике React:

  1. Создаёт новое fiber-дерево (render-фаза)
  2. Определяет, что изменился только текст {count} внутри <p>
  3. В commit-фазе обновляет один текстовый узел в реальном DOM

Батчинг обновлений

React группирует несколько вызовов setState в одно обновление — это называется batching. Так Virtual DOM перестраивается один раз, а не несколько:

function Counter() {
  const [count, setCount] = useState(0);
  const [doubled, setDoubled] = useState(0);

  function handleClick() {
    // React 18: оба setState объединятся в один рендер
    setCount(c => c + 1);
    setDoubled(c => c + 2);
  }

  return <button onClick={handleClick}>{count} / {doubled}</button>;
}

До React 18 батчинг работал только в обработчиках событий React. Начиная с React 18 — везде, включая setTimeout, fetch-коллбэки и нативные события.

Практический пример: почему важна структура компонентов

Virtual DOM эффективен только тогда, когда React умеет переиспользовать существующие узлы. Неправильная структура лишает его такой возможности:

// Плохо: компонент создаётся внутри другого компонента
// При каждом рендере Parent тип функции меняется,
// React думает, что это другой компонент, и монтирует заново
function Parent() {
  const Child = () => <div>Дочерний</div>; // новая функция каждый рендер!
  return <Child />;
}

// Хорошо: вынести компонент наружу
const Child = () => <div>Дочерний</div>;

function Parent() {
  return <Child />;
}

В первом варианте при каждом рендере Parent создаётся новая функция Child — React видит другой type и перемонтирует компонент, уничтожая его состояние.

Как Virtual DOM влияет на оптимизацию

Понимание Virtual DOM помогает правильно применять инструменты оптимизации:

import { memo, useCallback } from 'react';

// memo сравнивает пропсы между рендерами
// Если пропсы не изменились — компонент не перерендерится
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

function App() {
  const [selected, setSelected] = useState(null);
  const [filter, setFilter] = useState('');

  // useCallback сохраняет ссылку на функцию
  // Без него каждый рендер App создаёт новую функцию,
  // и memo на ExpensiveList не сработает
  const handleSelect = useCallback((id) => {
    setSelected(id);
  }, []);

  const filtered = items.filter(i => i.name.includes(filter));

  return (
    <>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      <ExpensiveList items={filtered} onSelect={handleSelect} />
    </>
  );
}

memo работает на уровне Virtual DOM: React пропускает фазу render для компонента, если пропсы не изменились, и не строит новое fiber-поддерево.

Ограничения Virtual DOM

Virtual DOM не является панацеей:

  • Накладные расходы на сравнение. Для очень маленьких приложений прямые манипуляции с DOM могут быть быстрее.
  • Память. React хранит два дерева одновременно (current и work-in-progress).
  • Не все операции ускоряются. Анимации, canvas, WebGL и прямой доступ к DOM (через ref) обходят Virtual DOM полностью.

Для случаев, когда Virtual DOM мешает, React предоставляет useRef и flushSync:

import { useRef } from 'react';

function VideoPlayer() {
  const videoRef = useRef(null);

  // Прямой доступ к DOM-элементу, минуя Virtual DOM
  function handlePlay() {
    videoRef.current.play();
  }

  return <video ref={videoRef} src="/video.mp4" />;
}

Итог

Virtual DOM — это слой абстракции между логикой приложения и реальным DOM браузера. Он позволяет React:

  • Декларативно описывать интерфейс через JSX
  • Батчить обновления и применять только минимальный набор изменений
  • Прерывать и приоритизировать работу через Fiber

Главное, что нужно помнить на практике: ключи в списках, стабильные типы компонентов и неизменяемость пропсов — три вещи, которые напрямую влияют на эффективность алгоритма сравнения.

Чтобы глубоко разобраться в работе React, жизненном цикле компонентов и оптимизации рендеров, пройдите курс React на PurpleSchool.

React с WebAssembly: интеграция Wasm-модулейСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

React тестирование с Vitest
React с TanStack Router
Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиReact с WebSocket — реалтайм в компонентахReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыОшибки гидратации в React и способы их устраненияHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияflushSync — синхронные обновления состояния в ReactuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий