Кэширование данных

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


Хуки состояния и мемоизация

Основной инструмент кэширования в Haunted — это сочетание useState, useEffect и useMemo.

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

Пример кэширования вычисленного значения:

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

function FibonacciComponent() {
  const [n, setN] = useState(10);

  const fib = useMemo(() => {
    const computeFib = (num) => {
      if (num <= 1) return num;
      return computeFib(num - 1) + computeFib(num - 2);
    };
    return computeFib(n);
  }, [n]);

  return html`
    <div>
      <p>Fibonacci(${n}) = ${fib}</p>
      <input type="number" @input=${e => setN(Number(e.target.value))} value=${n}>
    </div>
  `;
}

customElements.define('fibonacci-component', component(FibonacciComponent));

В этом примере вычисление Фибоначчи кэшируется, и функция computeFib вызывается только при изменении n. Без useMemo каждое обновление компонента инициировало бы повторный расчёт.


Кэширование асинхронных данных

Для работы с API или любыми асинхронными источниками данных важно избежать повторных запросов при каждом рендере. В Haunted кэширование асинхронных данных обычно строится на базе useState и useEffect, иногда в сочетании с глобальными структурами, такими как Map или WeakMap.

Пример кэширования данных с API:

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

const cache = new Map();

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (cache.has(userId)) {
      setUser(cache.get(userId));
      return;
    }

    fetch(`https://api.example.com/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        cache.set(userId, data);
        setUser(data);
      });
  }, [userId]);

  return html`
    <div>
      ${user ? html`<h2>${user.name}</h2>` : html`<p>Loading...</p>`}
    </div>
  `;
}

customElements.define('user-profile', component(UserProfile));

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


Локальное и глобальное кэширование

  • Локальный кэш хранится внутри компонента с помощью useState и подходит для данных, которые не нужны другим компонентам.
  • Глобальный кэш хранится вне компонента (например, в Map, WeakMap или в отдельном модуле) и используется, если данные повторяются в нескольких местах приложения.

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


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

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

  1. Мемоизировать вычисления с помощью useMemo.
  2. Кэшировать асинхронные результаты и использовать проверку на наличие данных в кэше перед новым запросом.
  3. Избегать создания функций внутри рендера, если они используются как колбэки зависимостей.

Пример оптимизации:

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

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

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

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

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

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


Кэширование сложных объектов

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

Пример с массивом:

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

function ItemsList({ items }) {
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => a.name.localeCompare(b.name));
  }, [items]);

  return html`
    <ul>
      ${sortedItems.map(item => html`<li>${item.name}</li>`)}
    </ul>
  `;
}

customElements.define('items-list', component(ItemsList));

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


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

  • Использовать глобальный кэш для часто повторяющихся асинхронных запросов.
  • Для вычислений, зависящих от состояния компонента, применять useMemo.
  • Для колбэков — useCallback.
  • Проверять зависимости useEffect и useMemo — неправильное указание может привести к бесконечным рендерам или устаревшим данным.
  • Очистка кэша при необходимости: для слабых ссылок (WeakMap) это делается автоматически, для Map и массивов — вручную.

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