Оптимизация ре-рендеров

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

Ререндеринг и его причины

Ререндеринг в Haunted происходит, когда изменяются реактивные состояния или свойства компонента. Основные источники изменений:

  • useState и useReducer — при вызове функции обновления состояния компонент повторно отрисовывается.
  • useContext — изменение контекста инициирует обновление всех зависимых компонентов.
  • Изменение атрибутов компонента напрямую через DOM.

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

Избегание ненужных ререндеров

  1. Мемоизация функций и значений

useMemo и useCallback позволяют сохранить вычисленные значения и функции между ререндерами, предотвращая их повторное создание:

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

function ExpensiveComponent({ items }) {
  const processedItems = useMemo(() => {
    return items.map(item => heavyComputation(item));
  }, [items]);

  return html`<ul>${processedItems.map(i => html`<li>${i}</li>`)}</ul>`;
}

customElements.define('expensive-component', component(ExpensiveComponent));

Здесь processedItems пересчитывается только при изменении массива items, а не при каждом обновлении компонента.

  1. Разделение состояния

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

const [count, setCount] = useState(0);
const [text, setText] = useState('');

Теперь изменение text не будет трогать визуализацию счётчика.

  1. Использование shouldUpdate в кастомных элементах

Haunted позволяет контролировать ререндер через метод shouldUpdate:

function MyComponent({ value }) {
  this.shouldUpdate = (prevProps, nextProps) => prevProps.value !== nextProps.value;

  return html`<p>${value}</p>`;
}
customElements.define('my-component', component(MyComponent));

Компонент перерисуется только при изменении конкретного свойства.

Оптимизация шаблонов

  1. Массивы и ключи элементов

При рендеринге списков желательно использовать стабильные ключи, чтобы Haunted корректно сопоставлял DOM-элементы:

const items = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
html`<ul>${items.map(item => html`<li key=${item.id}>${item.name}</li>`)}</ul>`;

Без ключей каждый элемент списка будет пересоздаваться заново при любом изменении массива.

  1. Минимизация inline-функций в шаблоне

Создание функций прямо в шаблоне приводит к их пересозданию на каждом ререндере. Лучше использовать useCallback или отдельные методы:

const handleClick = useCallback(() => console.log('clicked'), []);
html`<button @click=${handleClick}>Click</button>`;

Ленивая отрисовка и фрагментация

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

function Parent({ items }) {
  return html`
    <ul>
      ${items.map(item => html`<child-item .data=${item}></child-item>`)}
    </ul>
  `;
}

Каждый <child-item> ререндерится только при изменении своего собственного состояния или свойства data.

Влияние событий на ререндер

Haunted привязывает обработчики через шаблон. Следует избегать чрезмерного вызова setState внутри событий без необходимости:

button.addEventListener('input', () => setValue(e.target.value));

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

Профилирование производительности

Для анализа ререндеров можно использовать инструменты разработчика:

  • Performance tab в Chrome для отслеживания времени рендеров.
  • Логирование внутри useEffect или useLayoutEffect для контроля частоты обновлений.

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

Итоговые рекомендации по оптимизации

  • Разделять состояния и хранить только необходимое.
  • Мемоизировать вычисления и обработчики событий.
  • Использовать ключи при рендеринге списков.
  • Делить интерфейс на мелкие подкомпоненты.
  • Минимизировать inline-функции и создание новых объектов в шаблонах.
  • Контролировать ререндер через shouldUpdate при необходимости.

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