Отслеживание ре-рендеров

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


Хуки состояния и их влияние на ре-рендеры

В Haunted базовый механизм управления состоянием реализован через хук useState. Каждый раз, когда состояние обновляется, компонент вызывает повторный рендер. Пример:

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

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

  return html`
    <div>
      <p>Счетчик: ${count}</p>
      <button @click=${() => setCount(count + 1)}>Увеличить</button>
    </div>
  `;
}

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

В этом примере каждое изменение count приводит к перерисовке всего шаблона. Это поведение стандартное, но важно понимать, что Haunted не выполняет глубокого сравнения дерева элементов, как React с shouldComponentUpdate. Весь компонент рендерится заново при каждом изменении состояния.


Оптимизация через useMemo и useCallback

Haunted поддерживает хуки useMemo и useCallback, которые помогают контролировать создание объектов и функций между рендерами.

  • useMemo позволяет кешировать вычисления, чтобы тяжелые операции не выполнялись при каждом обновлении состояния.
import { useMemo } from 'haunted';

const factorial = n => (n <= 1 ? 1 : n * factorial(n - 1));

function Factorial({ value }) {
  const fact = useMemo(() => factorial(value), [value]);
  
  return html`<p>Факториал ${value}: ${fact}</p>`;
}
  • useCallback предотвращает создание новых функций на каждый рендер, что важно при передаче колбэков в дочерние компоненты.
import { useCallback } from 'haunted';

function Button({ onClick }) {
  return html`<button @click=${onClick}>Нажми</button>`;
}

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

  const increment = useCallback(() => setCount(c => c + 1), []);

  return html`<div>${count}<${Button} .onCl ick=${increment}></div>`;
}

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


Следование принципу «минимального рендера»

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

Пример разделения:

function Display({ value }) {
  return html`<p>${value}</p>`;
}

function Controls({ onIncrement }) {
  return html`<button @click=${onIncrement}>+1</button>`;
}

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

  return html`
    <${Display} .value=${count}></${Display}>
    <${Controls} .onIncrem ent=${() => setCount(c => c + 1)}></${Controls}>
  `;
}

Теперь, при изменении состояния, Haunted будет повторно рендерить только родительский компонент и дочерние элементы, но структура позволяет легко изолировать тяжелые части интерфейса.


Использование useRef для обхода лишних рендеров

Хук useRef сохраняет ссылку на объект между рендерами без триггера нового рендера. Это особенно полезно для хранения DOM-элементов или mutable объектов.

import { useRef, useEffect } from 'haunted';

function Timer() {
  const timerId = useRef(null);
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    timerId.current = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(timerId.current);
  }, []);

  return html`<p>Время: ${seconds}</p>`;
}

В этом примере объект timerId не вызывает повторный рендер, что позволяет хранить внутреннее состояние таймера без лишних обновлений интерфейса.


Слежение за свойствами компонента

Свойства веб-компонентов (props) также триггерят рендер. Haunted автоматически отслеживает их изменения, но можно использовать useEffect для реакции на конкретные изменения:

function Display({ value }) {
  useEffect(() => {
    console.log('value изменился на', value);
  }, [value]);

  return html`<p>${value}</p>`;
}

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


Практическая стратегия контроля рендеров

  1. Минимизировать количество состояний в компоненте.
  2. Выносить тяжелые вычисления в useMemo.
  3. Передавать функции через useCallback, чтобы не создавать новые объекты при каждом рендере.
  4. Делить компонент на логически независимые части, чтобы перерисовывать только необходимые блоки.
  5. Использовать useRef для хранения данных, не влияющих на UI.
  6. Следить за изменениями свойств через useEffect, чтобы выполнять побочные эффекты изолированно.

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