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

Библиотека Haunted использует концепцию реактивных компонентов на основе Web Components и хуков, аналогичных React, для управления состоянием и обновлениями интерфейса. Одной из ключевых задач в оптимизации производительности является батчинг обновлений, то есть группировка нескольких изменений состояния в один цикл перерисовки, чтобы минимизировать количество рефрешей DOM.

Механизм обновлений в Haunted

Каждый компонент Haunted обновляется при изменении реактивного состояния, созданного с помощью хука useState или useReducer. В стандартной работе каждый вызов setState инициирует синхронный рендер компонента, что может приводить к множественным перерисовкам при последовательном изменении состояния.

Haunted решает эту проблему за счёт асинхронного планирования обновлений. Компоненты регистрируют себя в очереди обновлений, и библиотека использует микротаски (Promise.resolve().then(...)) для объединения нескольких изменений в один вызов update.

import { html, component, useState } from 'haunted';

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

  const increment = () => {
    setCount(count + step);
    setStep(step + 1);
  };

  return html`
    <div>Count: ${count}</div>
    <div>Step: ${step}</div>
    <button @click=${increment}>Increment</button>
  `;
}

customElements.define('my-counter', component(Counter));

В этом примере два состояния обновляются одновременно через один обработчик. Благодаря внутреннему батчингу Haunted DOM будет обновлён один раз, несмотря на два вызова setState.

Отличие от синхронных обновлений

Если сравнивать с прямыми изменениями DOM или с обычными Web Components без Haunted, каждое изменение innerHTML или textContent выполняет отдельный рендер. Это приводит к лишним перерасходам ресурсов при множественных обновлениях. Батчинг позволяет:

  • Снижать нагрузку на движок рендеринга браузера.
  • Минимизировать мерцания интерфейса при последовательных обновлениях.
  • Объединять несколько действий в единый визуальный результат.

Внутренние очереди обновлений

Haunted использует две основные очереди:

  1. Локальная очередь компонентов, которые требуют перерисовки. Она отслеживает, какие экземпляры компонентов были изменены, чтобы обновить только их.
  2. Глобальная очередь микротасков, которая планирует вызов update на следующем tick события JavaScript, что гарантирует объединение всех синхронных вызовов setState.

Принцип работы можно представить как:

setState -> добавление компонента в локальную очередь -> планирование микротаска -> выполнение всех обновлений -> рендер компонентов

Прямое управление батчингом

Хотя Haunted автоматически батчит обновления, иногда требуется явное управление. Для этого можно использовать component.update или функцию queueMicrotask для планирования действий после всех текущих изменений.

setCount(count + 1);
queueMicrotask(() => {
  console.log('Все обновления компонента завершены');
});

Этот подход полезен при:

  • Сложных вычислениях на основе нескольких состояний.
  • Анимациях, зависящих от финального DOM после всех изменений.
  • Интеграции с внешними библиотеками, требующими точного момента обновления.

Оптимизация больших компонентов

Для больших и сложных компонентов батчинг становится критически важным. Рекомендуется:

  • Минимизировать прямые вызовы setState внутри циклов.
  • Использовать функциональные обновления (setState(prev => ...)) для корректного объединения нескольких изменений.
  • Выносить редко изменяемые части в отдельные компоненты, чтобы уменьшить количество обновлений в одном компоненте.
setCount(prev => prev + 1);
setStep(prev => prev + 2);

Использование функциональных обновлений гарантирует, что каждое изменение будет корректно учтено в рамках одного батча.

Взаимодействие с хуками

Батчинг тесно связан с хуками Haunted:

  • useState и useReducer интегрированы с внутренней очередью обновлений.
  • useEffect выполняется после завершения всех батчей, что позволяет безопасно работать с DOM после перерисовки.
  • useRef остаётся стабильным между обновлениями, позволяя хранить данные вне батча без лишних рендеров.

Пример комплексного батча

function ComplexCounter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);
  const [log, setLog] = useState([]);

  const incrementAll = () => {
    setCount(prev => prev + step);
    setStep(prev => prev + 1);
    setLog(prev => [...prev, `Count: ${count + step}, Step: ${step + 1}`]);
  };

  return html`
    <div>Count: ${count}</div>
    <div>Step: ${step}</div>
    <button @click=${incrementAll}>Increment All</button>
    <ul>${log.map(item => html`<li>${item}</li>`)}</ul>
  `;
}

В этом случае три состояния обновляются одновременно, но Haunted гарантирует один рендер компонента, объединяя все изменения.

Батчинг обновлений является фундаментальной оптимизацией Haunted, позволяющей создавать производительные интерфейсы на базе Web Components с минимальными затратами на перерисовку DOM.