Оптимизация перерендеринга

Понимание перерендеринга

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

Ключевой момент: рендеринг — это не всегда обновление реального DOM. Preact использует виртуальный DOM, чтобы минимизировать прямые манипуляции с DOM, но вычислительные затраты на создание виртуальных деревьев остаются.

PureComponent и мемоизация компонентов

Для предотвращения лишних перерендерингов используется мемоизация компонентов. Preact предоставляет функцию memo, аналогичную React.memo, которая позволяет обернуть функциональный компонент и ререндерить его только при изменении переданных props.

import { h, memo } from 'preact';

const Button = memo(({ label, onClick }) => {
  console.log('Button render');
  return <button onCl ick={onClick}>{label}</button>;
});

Особенности использования:

  • memo сравнивает props по ссылке. Если передается новый объект при каждом рендере, мемоизация не сработает.
  • Для сложных объектов можно использовать кастомную функцию сравнения:
const Button = memo(
  ({ label, config }) => <button>{label}</button>,
  (prevProps, nextProps) => prevProps.label === nextProps.label && prevProps.config.id === nextProps.config.id
);

Использование хуков для оптимизации

useCallback

Функция useCallback позволяет мемоизировать обработчики событий, чтобы они не создавались заново при каждом рендере. Это критично для компонентов, обернутых в memo.

import { useState, useCallback } from 'preact/hooks';

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

  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return <Button label={count} onCl ick={increment} />;
};
useMemo

useMemo используется для мемоизации вычисляемых значений, которые требуют затратных операций.

const expensiveCalculation = useMemo(() => {
  return heavyComputation(data);
}, [data]);

Без useMemo каждая перерисовка компонента заново вычисляла бы heavyComputation, что увеличивает нагрузку на CPU.

Разделение компонентов

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

const Parent = ({ data }) => {
  return (
    <div>
      <Header title={data.title} />
      <Content items={data.items} />
    </div>
  );
};

const Header = memo(({ title }) => <h1>{title}</h1>);
const Content = memo(({ items }) => <ul>{items.map(item => <li>{item}</li>)}</ul>);

Оптимизация списков

При работе с динамическими списками важно использовать корректные ключи (key). Preact использует ключи для минимизации изменений DOM.

<ul>
  {items.map(item => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>

Правила выбора ключей:

  • Не использовать индекс массива как ключ, если порядок элементов может изменяться.
  • Предпочтение отдавать уникальным и стабильным идентификаторам.

Отложенный рендеринг и виртуализация

Для длинных списков и таблиц используется виртуализация, когда рендерятся только элементы, видимые на экране. Это значительно снижает нагрузку на рендер и повышает отзывчивость интерфейса. В Preact можно использовать сторонние библиотеки, совместимые с его API, например preact-virtual-list.

import VirtualList from 'preact-virtual-list';

<VirtualList
  data={items}
  rowHeight={50}
  rowRenderer={(item) => <div>{item.name}</div>}
/>

Отслеживание источников лишних ререндерингов

  • Избыточная генерация объектов и массивов в JSX:
<Component options={[1,2,3]} /> // Создаётся новый массив при каждом рендере

Решение: мемоизация через useMemo или вынесение констант вне компонента.

  • Изменение функций обработчиков без мемоизации.
  • Передача props как анонимных функций или объектов.

Заключение по стратегии оптимизации

  • Минимизировать глубину перерендеринга через мемоизацию компонентов.
  • Использовать useCallback и useMemo для обработки функций и вычислений.
  • Разделять интерфейс на мелкие компоненты с четкими границами данных.
  • Правильно задавать ключи в списках.
  • Применять виртуализацию для больших наборов данных.
  • Анализировать производительность с помощью инструментов вроде preact/debug и профайлера браузера.

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