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

Что такое мемоизация и когда её следует применять?

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

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

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

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

Мемоизация и её применение

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

Основная идея

// Без мемоизации
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// fibonacci(5) вычисляет:
// fibonacci(4) и fibonacci(3)
// fibonacci(4) = fibonacci(3) + fibonacci(2) (fibonacci(3) вычисляется СНОВА!)
// Много повторных вычислений

console.time('without memo');
fibonacci(40); // Займёт несколько секунд!
console.timeEnd('without memo');
// С мемоизацией
const memo = {};

function fibonacciMemo(n) {
  if (n in memo) return memo[n]; // Возвращаю кэш
  if (n <= 1) return n;
  
  const result = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
  memo[n] = result; // Сохраняю результат
  return result;
}

console.time('with memo');
fibonacciMemo(40); // Мгновенно!
console.timeEnd('with memo');
// Результат: fibonacci(40) вычисляется один раз для каждого n

Создание функции-обёртки для мемоизации

// Универсальная функция мемоизации
function memoize(fn) {
  const cache = {};
  
  return function(...args) {
    // Создаю ключ кэша из аргументов
    const key = JSON.stringify(args);
    
    // Если результат уже есть в кэше
    if (key in cache) {
      console.log(`Возвращаю из кэша для ${key}`);
      return cache[key];
    }
    
    // Иначе вычисляю результат
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

// Использование
const add = (a, b) => {
  console.log(`Вычисляю ${a} + ${b}`);
  return a + b;
};

const memoizedAdd = memoize(add);

memoizedAdd(5, 3); // Вычисляю 5 + 3 → 8
memoizedAdd(5, 3); // Возвращаю из кэша → 8 (без вывода консоли)
memoizedAdd(2, 4); // Вычисляю 2 + 4 → 6

Мемоизация в React: useMemo

import { useMemo } from 'react';

function MyComponent({ items }) {
  // Без мемоизации: вычисляется при каждом рендере
  const expensiveCalculation = items.reduce((sum, item) => {
    console.log('Вычисляю sum...');
    return sum + item.value;
  }, 0);
  
  return <div>Сумма: {expensiveCalculation}</div>;
}

// С мемоизацией: вычисляется только когда items изменяется
function MyComponentOptimized({ items }) {
  const expensiveCalculation = useMemo(() => {
    console.log('Вычисляю sum...');
    return items.reduce((sum, item) => sum + item.value, 0);
  }, [items]); // зависимость: items
  
  return <div>Сумма: {expensiveCalculation}</div>;
}

Мемоизация React компонентов: React.memo

// Компонент, который перерендеривается даже если props не изменились
function UserCard({ user }) {
  console.log('Рендерю UserCard для', user.name);
  return <div>{user.name} - {user.email}</div>;
}

// Мемоизированный компонент
const MemoizedUserCard = React.memo(UserCard);

function App() {
  const [count, setCount] = React.useState(0);
  const user = { name: 'Alice', email: 'alice@example.com' };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Счётчик: {count}</button>
      <MemoizedUserCard user={user} />
      {/* UserCard рендерится только когда меняется user */}
    </div>
  );
}

Мемоизация обработчиков: useCallback

function Parent() {
  const [count, setCount] = React.useState(0);
  
  // ❌ Без мемоизации: новая функция при каждом рендере
  const handleClick = () => {
    console.log('Нажата кнопка');
  };
  
  // ✅ С мемоизацией: одна функция, пока зависимости не изменятся
  const memoizedHandleClick = React.useCallback(() => {
    console.log('Нажата кнопка');
  }, []); // нет зависимостей
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Счётчик: {count}</button>
      <Child onClick={memoizedHandleClick} />
    </div>
  );
}

const Child = React.memo(({ onClick }) => {
  console.log('Рендерю Child');
  return <button onClick={onClick}>Дочерняя кнопка</button>;
});

Когда применять мемоизацию

1. Дорогостоящие вычисления

// Расчёт статистики по большому массиву
const calculateStats = useMemo(() => {
  return data.map(item => ({
    ...item,
    percentile: calculatePercentile(item.value, data),
  }));
}, [data]);

2. Функции, передаваемые как props

// ❌ Плохо: новая функция при каждом рендере
function Parent() {
  return <Expensive onClick={() => console.log('clicked')} />;
}

// ✅ Хорошо: стабильная функция
function Parent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);
  return <Expensive onClick={handleClick} />;
}

3. Частые перерендеры, редкие изменения данных

// Компонент перерендеривается часто, но props меняются редко
const ExpensiveComponent = React.memo(function({ largeList }) {
  const processedList = useMemo(() => {
    return largeList.filter(item => item.active).sort();
  }, [largeList]);
  
  return (
    <ul>
      {processedList.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
});

4. Длинные цепочки зависимостей

function Dashboard({ userId, filters, timeRange }) {
  // Мемоизирую объект, чтобы не создавалась новая ссылка
  const queryParams = useMemo(() => ({
    userId,
    ...filters,
    startDate: timeRange.start,
    endDate: timeRange.end,
  }), [userId, filters, timeRange]);
  
  // Используется в useEffect
  useEffect(() => {
    fetchData(queryParams);
  }, [queryParams]);
}

Когда НЕ использовать мемоизацию

1. Простые вычисления

// ❌ Излишняя мемоизация
const sum = useMemo(() => a + b, [a, b]);

// ✅ Просто вычисли
const sum = a + b;

2. Примитивные значения в props

// ❌ Не нужна React.memo
const Button = React.memo(({ label, count }) => {
  return <button>{label} {count}</button>;
});

// ✅ Просто компонент
function Button({ label, count }) {
  return <button>{label} {count}</button>;
}

3. Неправильные зависимости

// ❌ Плохо: зависимость неправильная
const result = useMemo(() => {
  return data.filter(item => item.userId === userId);
}, [data]); // забыли userId!

// ✅ Правильно
const result = useMemo(() => {
  return data.filter(item => item.userId === userId);
}, [data, userId]);

Практический пример: фильтрация большого списка

function UserList({ users, searchTerm }) {
  // Мемоизирую фильтрованный список
  const filteredUsers = useMemo(() => {
    console.log('Фильтрую пользователей...');
    return users.filter(user =>
      user.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [users, searchTerm]);
  
  return (
    <div>
      {filteredUsers.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

При каждом введении символа в поле поиска:

  • Без мемоизации: фильтрация выполняется и таблица перерендеривается
  • С мемоизацией: фильтрация выполняется только если searchTerm изменился

Правила мемоизации

  1. Не переусложняй: мемоизация имеет свою стоимость (память, время на сравнение)
  2. Измеряй производительность: используй DevTools Profiler
  3. Задай правильные зависимости: иначе мемоизация не будет работать
  4. Избегай создания новых объектов в зависимостях:
// ❌ Плохо: { name: 'Alice' } создаётся заново при каждом рендере
const result = useMemo(() => {
  return processUser({ name: 'Alice' });
}, [{ name: 'Alice' }]); // новый объект = пересчёт

// ✅ Хорошо
const user = useMemo(() => ({ name: 'Alice' }), []);
const result = useMemo(() => {
  return processUser(user);
}, [user]);

Итоги

  • Мемоизация — кэширование результатов функций
  • Применяй для дорогостоящих вычислений, частых перерендеров
  • В React: useMemo для значений, useCallback для функций, React.memo для компонентов
  • Не переусложняй: профилируй перед оптимизацией
  • Правильные зависимости — ключ к успеху