Блеск и нищета Virtual DOM. Почему React работает быстро, но не так быстро как хотелось бы

Frontend

Если вас спросят, какое решение в свое время сделало React прорывной библиотекой для строительства веб-интерфейсов, то вы, скорее всего ответите — виртуальный DOM. И будете правы. Разработчики React первыми сделали ставку на эту концепцию и не прогадали. Но время идет, фронтенд развивается и сегодня есть альтернативные решения. Об этом и поговорим.

Содержание
  1. Почему операции над DOM столь медленны
  2. Как работает Virtual DOM: пошагово
  3. Шаг 1. Создание Virtual DOM
  4. Шаг 2. Изменение состояния
  5. Шаг 3. Сравнение
  6. Шаг 4. Применение изменений
  7. Сильные стороны Virtual DOM
  8. 1. Производительность при частых обновлениях
  9. 2. Декларативность
  10. 3. Кросс-платформенность
  11. 4. Упрощение отладки и разработки
  12. 5. Простота освоения
  13. Слабые стороны Virtual DOM
  14. 1. Память: два дерева вместо одного
  15. 2. Начальный рендеринг медленнее
  16. 3. Алгоритм сравнения не идеален
  17. 4. Каждое изменение — пересоздание виртуального дерева (Overhead)
  18. 5. Сложность с большими деревьями элементов
  19. Альтернативы Virtual DOM
  20. 1. Реальный DOM с оптимизациями (Vanilla JS)
  21. 2. Отрисовка без виртуализации — Svelte
  22. 3. Сигналы и реактивность — SolidJS
  23. 4. Incremental DOM (Angular)
  24. 5. «No DOM» — веб-компоненты (Lit)
  25. Сравнительная таблица
  26. Когда Virtual DOM — это хорошо, а когда — плохо
  27. Virtual DOM хорош, если:
  28. Virtual DOM плох, если:
  29. Итоги

Почему операции над DOM столь медленны

DOM (Document Object Model) — это представление вашей HTML-страницы в виде дерева объектов. Браузер строит и хранит это дерево в памяти. Когда вы меняете DOM, браузер должен:

  1. Перестроить дерево DOM.
  2. Пересчитать стили (CSS Recalc).
  3. Перерисовать пиксели на экране (Repaint).
  4. Сдвинуть элементы (Reflow).

Это дорого. Особенно когда изменений много. Например, вам нужно обновлять список из 1000 элементов каждую секунду.

Решение той же задачи с Virtual DOM будет выглядеть иначе: вместо того чтобы 1000 раз изменять DOM, мы 1000 раз произведем изменения в памяти и затем всего один раз обновим реальный DOM.

Как работает Virtual DOM: пошагово

Шаг 1. Создание Virtual DOM

React создает легковесную копию реального DOM в памяти. Это знакомые нам JavaScript-объекты, у которых есть свойства (тип элемента, пропсы, дети). Они не имеют никакого отношения к браузерному DOM.

// Пример Virtual DOM (упрощенно)
const vdom = {
    type: 'div',
    props: { className: 'app' },
    children: [
        { type: 'h1', props: {}, children: ['Hello'] }
    ]
};

Шаг 2. Изменение состояния

Вы меняете состояние. React создает новый Virtual DOM, отражающий новое состояние. Старый Virtual DOM пока что остается в памяти для сравнения.

Шаг 3. Сравнение

React сравнивает старый и новый Virtual DOM. Он использует алгоритм сравнения (reconciliation), который находит минимальное количество изменений, которые необходимы чтобы «было» превратить в «стало».

Шаг 4. Применение изменений

React берет список изменений и применяет их к реальному DOM. Он делает это пакетами (batching), чтобы минимизировать количество перерисовок в браузере.

Сильные стороны Virtual DOM

1. Производительность при частых обновлениях

Если вы обновляете состояние 100 раз за секунду, React не будет 100 раз трогать реальный DOM. Он соберет все изменения, сгруппирует их и применит один раз. Браузер перерисует страницу 1 раз вместо 100.

2. Декларативность

Вы пишете: «Вот что должно быть». React сам решает, как это сделать эффективно. Вам не нужно писать document.getElementById и вручную править DOM. Код чище и понятнее.

3. Кросс-платформенность

Virtual DOM — это просто JavaScript-объекты. React Native использует тот же подход, но вместо реального DOM рендерит нативные компоненты iOS/Android. Поэтому мы можем запустить реакт-приложение можно запустить на многих-многих платформах.

4. Упрощение отладки и разработки

Все изменения хранятся в памяти, поэтому можно даже логировать Virtual DOM (но лучше все же через девтулзы — так нагляднее и удобнее), путешествовать во времени, переключая состояния (time-travel debugging).

5. Простота освоения

Вам не нужно думать о DOM-операциях. Вы просто описываете компоненты.

Слабые стороны Virtual DOM

1. Память: два дерева вместо одного

React хранит в памяти старый и новый Virtual DOM одновременно. Для больших приложений (тысячи компонентов) это может занимать десятки мегабайт. На мобильных устройствах с ограниченной памятью это вероятнее всего станет проблемой.

2. Начальный рендеринг медленнее

React сначала строит Virtual DOM, потом на его основе строит реальный DOM. Это занимает время.

3. Алгоритм сравнения не идеален

React использует алгоритм сравнения деревьев, который работает за O(n) (линейное время). Он хорош, но есть и некоторые особенности:

  • Он сравнивает элементы по типу. Если вы поменяли <div> на <span>, поддерево пересоздастся целиком.
  • Для списков ему нужны ключи — те самые key. Без них можно, но производительность падает.

4. Каждое изменение — пересоздание виртуального дерева (Overhead)

Даже если вы изменили одно поле в объекте, React все равно пересоздает виртуальное дерево, сравнивает и применяет патч. Это делает Virtual DOM избыточным для тех приложений, где страница обновляется нечасто.

5. Сложность с большими деревьями элементов

Если у вас список из 10 000 элементов, и вы меняете один из них, React все равно сравнивает все 10 000 элементов.

Альтернативы Virtual DOM

1. Реальный DOM с оптимизациями (Vanilla JS)

При таком подходе мы откатываемся назад во времени, отказываемся от всех благ прогресса, и просто работаем напрямую с DOM, стараясь делать это аккуратно.

const list = document.getElementById('list');
list.innerHTML = items.map(item => `<li>${item}</li>`).join('');

Плюсы: Нет лишней памяти (как у Virtual DOM). Быстрее для простых страниц.
Минусы: Сложно поддерживать большие приложения. Нет реактивности. Можно легко наделать ошибок.

2. Отрисовка без виртуализации — Svelte

Svelte — компилятор, который превращает ваши компоненты в императивный код, работающий напрямую с DOM. Он делает всю работу на этапе сборки. В рантайме нет Virtual DOM, нет какого бы то ни было фреймворка — только чистый JavaScript.

<script>
    let count = 0;
    function increment() {
        count += 1;
    }
</script>
<button on:click={increment}>
    Clicks: {count}
</button>

Svelte компилирует это в код, который при изменении count напрямую обновляет только текст внутри кнопки. Без Virtual DOM, без сравнения, без лишнего оверхеда.

Плюсы: Быстрее, меньше памяти, меньше размера бандла.
Минусы: Гораздо более слабая экосистема, чем у React.

3. Сигналы и реактивность — SolidJS

SolidJS тоже использует компиляцию, но в стиле React-подобного JSX. У него нет Virtual DOM, но есть «сигналы». Изменение сигнала обновляет только зависимые части DOM.

import { createSignal } from "solid-js";
function Counter() {
    const [count, setCount] = createSignal(0);
    return <button onClick={() => setCount(count() + 1)}>Clicks: {count()}</button>;
}

Solid обновляет только текст внутри кнопки, без Virtual DOM. Он знает, какие части DOM зависят от каких сигналов.

Плюсы: Очень быстрый (близко к производительности нативного JS). Меньше памяти.
Минусы: Меньше тулинг и библиотек, чем у React.

4. Incremental DOM (Angular)

Angular использует Incremental DOM — подход, при котором изменения применяются к реальному DOM по частям. Каждый компонент знает, как обновить себя, не создавая полной копии дерева.

Плюсы: Требует меньше памяти.
Минусы: Сложнее оптимизировать и отлаживать.

5. «No DOM» — веб-компоненты (Lit)

Lit — библиотека для веб-компонентов, которая использует шаблонные литералы и обновляет только измененные части DOM через систему реактивности.

import { LitElement, html } from 'lit';
class MyElement extends LitElement {
    static properties = { count: {} };
    render() {
        return html`<button @click=${() => this.count++}>Clicks: ${this.count}</button>`;
    }
}

Плюсы: Нативная поддержка браузеров. Меньший размер.
Минусы: Слабая экосистема.

Сравнительная таблица

ТехнологияVirtual DOMТребования к памятиСкорость обновленияРазмер бандла
React (Virtual DOM)✅ ЕстьВысокиеВысокаяБольшой
Svelte (No Virtual DOM)❌ НетНизкиеОчень высокаяМаленький
SolidJS (Сигналы)❌ НетНизкиеОчень высокаяСредний
Angular (Incremental DOM)❌ НетСредниеВысокаяБольшой
Lit (No DOM)❌ НетНизкиеВысокаяМаленький
Vanilla JS (Прямой DOM)❌ НетНизкиеЗависит от кодаМинимальный

Когда Virtual DOM — это хорошо, а когда — плохо

Virtual DOM хорош, если:

  • Приложение сложное (много компонентов, много состояний)
  • Вы часто обновляете данные (например, чат, биржевой терминал)
  • Вам нужна кросс-платформенность
  • Вы цените удобство разработки и быстрый старт

Virtual DOM плох, если:

  • Приложение простое (лендинг, визитка)
  • У вас есть ограничения по памяти (мобильные приложения на старых устройствах)
  • Вы стремитесь к максимальной производительности
  • Бандл должен быть минимальным (например, это важно для встраивания на чужой сайт).

Итоги

React выбирают не за то, что он самый быстрый. Выбирают за удобство разработки и богатую, наработанную за годы, экосистему. Альтернативы вроде Svelte и Solid быстрее, но для них существует меньше библиотек, да и разработчиков найти сложнее. Решать что использовать, как обычно, вам.

Желаю успехов!

Симо Мофин
Симо Мофин

Senior Frontend Developer
Главный по блогу