← Назад к вопросам

Для чего кешировать функцию?

2.2 Middle🔥 221 комментариев
#JavaScript Core#Оптимизация и производительность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Кеширование функции — оптимизация производительности

Кеширование функции (memoization) — это техника оптимизации, которая сохраняет результаты дорогостоящих вычислений для одних и тех же входных данных. Вместо повторного вычисления функция возвращает сохранённый результат.

Зачем нужно кеширование

// ❌ Без кеширования — медленно
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

console.time("fib");
console.log(fibonacci(40)); // Очень долго считает
console.timeEnd("fib"); // ~10 секунд

// ✅ С кешированием — быстро
function memoFibonacci(n: number, cache: Record<number, number> = {}): number {
  if (n in cache) {
    console.log(`Возвращаем закешированное значение ${n}`);
    return cache[n];
  }

  if (n <= 1) return n;

  cache[n] = memoFibonacci(n - 1, cache) + memoFibonacci(n - 2, cache);
  return cache[n];
}

console.time("memoFib");
console.log(memoFibonacci(40)); // Почти мгновенно
console.timeEnd("memoFib"); // ~1 миллисекунда

Основные причины кеширования

  1. Производительность — избегаем дорогостоящих вычислений
  2. Сокращение нагрузки на сервер — кешируем результаты API запросов
  3. Улучшение UX — быстрый отклик приложения
  4. Экономия батареи и памяти — меньше обработок
  5. Работа offline — используем закешированные данные

Кеширование в React: useMemo

import { useMemo } from "react";

interface Product {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

function ShoppingCart({ products }: { products: Product[] }) {
  // ❌ Без useMemo — пересчитывается при каждом рендере
  const total = products.reduce((sum, p) => sum + (p.price * p.quantity), 0);

  // ✅ С useMemo — пересчитывается только если products изменился
  const memoTotal = useMemo(() => {
    console.log("Пересчитываем total");
    return products.reduce((sum, p) => sum + (p.price * p.quantity), 0);
  }, [products]);

  const taxAmount = useMemo(() => {
    console.log("Пересчитываем налог");
    return memoTotal * 0.1;
  }, [memoTotal]);

  return (
    <div>
      <p>Сумма товаров: \${memoTotal}</p>
      <p>Налог: \${taxAmount}</p>
      <p>Итого: \${memoTotal + taxAmount}</p>
    </div>
  );
}

// Если products не изменился, useMemo вернёт старое значение
// "Пересчитываем total" не выведется

Кеширование компонентов: React.memo

import { memo } from "react";

interface UserCardProps {
  userId: number;
  onSelect: (id: number) => void;
}

// ❌ Без memo — перерисовывается при каждом рендере родителя
function UserCardUnmemoized({ userId, onSelect }: UserCardProps) {
  console.log(`Рендер UserCard ${userId}`);
  return (
    <div>
      <h3>Пользователь {userId}</h3>
      <button onClick={() => onSelect(userId)}>Выбрать</button>
    </div>
  );
}

// ✅ С memo — перерисовывается только если props изменились
const UserCard = memo(function UserCard({ userId, onSelect }: UserCardProps) {
  console.log(`Рендер UserCard ${userId}`);
  return (
    <div>
      <h3>Пользователь {userId}</h3>
      <button onClick={() => onSelect(userId)}>Выбрать</button>
    </div>
  );
});

// В родительском компоненте
function UserList() {
  const [selected, setSelected] = useState<number | null>(null);
  const users = [1, 2, 3];

  // Проблема: обработчик создаётся заново при каждом рендере
  const handleSelect = (id: number) => {
    setSelected(id);
  };

  return (
    <div>
      {users.map(userId => (
        <UserCard 
          key={userId}
          userId={userId}
          onSelect={handleSelect} // Новая функция каждый раз
        />
      ))}
    </div>
  );
}

useCallback — кеширование функции

import { useCallback } from "react";

function UserList() {
  const [selected, setSelected] = useState<number | null>(null);
  const users = [1, 2, 3];

  // ❌ Без useCallback — новая функция при каждом рендере
  const handleSelectBad = (id: number) => {
    setSelected(id);
  };

  // ✅ С useCallback — одна и та же функция пока зависимостей нет
  const handleSelectGood = useCallback((id: number) => {
    setSelected(id);
  }, []); // Зависимости пусты, функция никогда не пересоздаётся

  return (
    <div>
      {users.map(userId => (
        <UserCard 
          key={userId}
          userId={userId}
          onSelect={handleSelectGood}
        />
      ))}
    </div>
  );
}

Кеширование API запросов

import { useState, useEffect } from "react";

// ✅ Простой кеш для API запросов
const apiCache = new Map<string, Promise<any>>();

async function cachedFetch(url: string): Promise<any> {
  // Если результат уже в кеше, вернуть его
  if (apiCache.has(url)) {
    console.log(`Возвращаем кешированный результат для ${url}`);
    return apiCache.get(url);
  }

  // Иначе, сделать запрос и кешировать
  console.log(`Делаем запрос ${url}`);
  const promise = fetch(url)
    .then(r => r.json())
    .catch(err => {
      // Если ошибка, удалить из кеша
      apiCache.delete(url);
      throw err;
    });

  apiCache.set(url, promise);
  return promise;
}

// Использование в компоненте
function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<any>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    cachedFetch(`/api/users/${userId}`)
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <p>Загрузка...</p>;
  return <div>{user?.name}</div>;
}

// Кеш будет работать на протяжении всего жизненного цикла приложения
// Но для production нужна более сложная стратегия (TTL, размер и т.п.)

Реализация простого кеширования функции

// Функция для создания кешированной версии
function memoize<T extends (...args: any[]) => any>(fn: T): T {
  const cache = new Map();

  return ((...args: any[]) => {
    // Создаём ключ из аргументов
    const key = JSON.stringify(args);

    // Если в кеше, вернуть
    if (cache.has(key)) {
      console.log("Из кеша:", key);
      return cache.get(key);
    }

    // Иначе, вычислить и кешировать
    const result = fn(...args);
    cache.set(key, result);
    console.log("Вычислено:", key);
    return result;
  }) as T;
}

// Дорогостоящая функция
function expensiveCalculation(a: number, b: number): number {
  console.time("Вычисление");
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += i % (a + b);
  }
  console.timeEnd("Вычисление");
  return result;
}

const memoExpensive = memoize(expensiveCalculation);

memoExpensive(5, 10); // Вычисление ~ 500мс
memoExpensive(5, 10); // Из кеша ~ 0мс (мгновенно)
memoExpensive(5, 11); // Вычисление ~ 500мс (другие аргументы)

Когда НЕ использовать кеширование

// ❌ Бессмысленно кешировать быстрые операции
const memoAdd = useMemo(() => 2 + 2, []); // Зачем? Это быстро

// ❌ Кеширование занимает больше памяти, чем сэкономит
function SearchResults({ term }: { term: string }) {
  // Если term меняется часто, кеш не поможет
  const results = useMemo(() => {
    return expensiveSearch(term);
  }, [term]);
}

// ❌ Помните о пропускной способности памяти
const cache = new Map();
for (let i = 0; i < 1000000; i++) {
  cache.set(i, new Array(10000).fill(i)); // Утечка памяти!
}

Лучшие практики

  • Кешируйте только дорогостоящие вычисления
  • Помните о размере кеша — может кончиться память
  • Используйте TTL (Time To Live) для инвалидации устаревших данных
  • Тестируйте производительность перед и после кеширования
  • Не переусложняйте — иногда без кеша быстрее