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

Батчинг обновлений — ключевая оптимизация, которая позволяет Preact минимизировать количество операций с виртуальным DOM и реальным DOM, улучшая производительность приложений. В основе механизма лежит агрегирование нескольких изменений состояния в одну транзакцию, что снижает накладные расходы на перерасчёт и перерисовку компонентов.

Принцип работы

Когда состояние компонента изменяется через setState или хуки (useState), Preact не обновляет DOM немедленно. Вместо этого изменения помещаются в очередь. После того как текущий стек вызовов JavaScript завершает выполнение, очередь обрабатывается единым батчем:

  • Все изменения состояния, произошедшие в рамках одного события или синхронного блока кода, объединяются.
  • Preact вычисляет минимальный набор обновлений виртуального DOM.
  • Реальный DOM изменяется только один раз для всех накопленных изменений.

Это снижает количество лишних рендеров и позволяет поддерживать высокую скорость работы интерфейса.

Сценарии синхронного и асинхронного батчинга

Синхронный батчинг возникает внутри обработчиков событий DOM. Например:

import { h } from 'preact';
import { useState } from 'preact/hooks';

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

  const increment = () => {
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
  };

  return <button onCl ick={increment}>{count}</button>;
}

Несмотря на три вызова setCount, Preact объединяет их в один рендер. Конечное значение count увеличивается на 3 за один проход.

Асинхронный батчинг применяется в промисах, setTimeout, requestAnimationFrame или других отложенных вызовах:

setTimeout(() => {
  setCount(c => c + 1);
  setCount(c => c + 1);
}, 0);

В таких случаях каждое изменение состояния может быть обработано отдельно, так как события происходят в разных стэках вызовов. Для контроля можно использовать ручной батчинг через функцию unstable_batchedUpdates (аналогичный React API):

import { unstable_batchedUpdates } from 'preact/compat';

setTimeout(() => {
  unstable_batchedUpdates(() => {
    setCount(c => c + 1);
    setCount(c => c + 1);
  });
}, 0);

Механизм оптимизации рендеров

Батчинг тесно связан с механизмом виртуального DOM и стратегиями diffing. Preact сравнивает предыдущий и новый виртуальный DOM и вычисляет минимальный набор изменений для реального DOM:

  1. Компоненты обновляются только при изменении props или state.
  2. Одно и то же обновление состояния внутри батча не вызывает множественных перерисовок дочерних компонентов.
  3. Preact избегает ненужного удаления и вставки элементов DOM, выполняя локальные операции изменения.

Особенности работы с хуками

При использовании хуков батчинг сохраняет последовательность вызовов:

  • useState и useReducer собирают обновления до следующего рендера.
  • useEffect и useLayoutEffect выполняются после того, как все обновления батча завершены, что гарантирует согласованность состояния и DOM.
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';

function Timer() {
  const [time, setTime] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setTime(t => t + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return <div>{time}</div>;
}

В данном примере состояние time обновляется каждую секунду, но DOM перерисовывается только после завершения каждой итерации батча, что предотвращает лишние промежуточные рендеры.

Практические рекомендации

  • Группировать несколько обновлений состояния в рамках одного синхронного события.
  • Использовать функциональный вариант setState, чтобы гарантировать корректное накопление изменений в батче.
  • Для асинхронных вызовов применять unstable_batchedUpdates, если требуется объединение обновлений.
  • Минимизировать глубокие вложенные структуры состояния и избегать частых изменений больших объектов, так как это увеличивает вычислительную нагрузку при diffing.

Батчинг обновлений позволяет Preact сохранять лёгкость и высокую производительность, одновременно упрощая управление состоянием и снижая количество ненужных рендеров компонентов. Эффективное использование этого механизма особенно важно в масштабных приложениях с динамическими интерфейсами и высокой частотой событий.