Гид по технологиям

Мемоизация в JavaScript и React: руководство по оптимизации

6 min read Frontend Обновлено 30 Dec 2025
Мемоизация в JavaScript и React
Мемоизация в JavaScript и React

Чёрно‑жёлтый электронный чип на микросхеме

Зачем нужна мемоизация

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

Кратко:

  • Оптимизирует время при повторных вызовах с одинаковыми аргументами.
  • Увеличивает потребление памяти (кеш) — это компромисс «память за счёт скорости».
  • Работает корректно только для чистых функций: одинаковые входы → одинаковый выход.

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

Мемоизация в чистом JavaScript

Идея простая: храните результаты в объекте/Map и возвращайте кешированное значение, если такой ключ уже есть.

Простой пример функции, возвращающей квадрат числа:

function square(num) {
  return num * num;
}

// Вызов
square(5); // 25

Если вычисление было бы тяжёлым, можно добавить мемоизацию:

const memoizedSquare = (() => {
  let cache = {};
  return (num) => {
    if (num in cache) {
      console.log('Reusing cached value');
      return cache[num];
    } else {
      console.log('Calculating result');
      let result = num * num;
      // кешируем результат для следующего раза
      cache[num] = result;
      return result;
    }
  };
})();

// Использование
memoizedSquare(50000); // Calculating result
memoizedSquare(50000); // Reusing cached value

Тонкости и улучшения:

  • Для сложных ключей (объекты, массивы) используйте стабильное сериализованное представление ключа или Map с композитными ключами.
  • Map предпочтительнее объекта, если ключом может быть не строка.
  • Добавьте политику сброса кеша (LRU, TTL), если память ограничена.

Когда мемоизация работает плохо — контрпримеры

  • Функция зависит от внешнего состояния (нечистая), например, использует Math.random() или дату: кеш вернёт устаревшее значение.
  • Аргументы функции часто уникальны: кеш никогда не переиспользуется, а накладные расходы увеличивают время.
  • Стоимость сравнения аргументов перевешивает выгоду от кеша, особенно для вложенных объектов без лёгкого способа сравнения.
  • Ограниченная память: бесконтрольный рост кеша приведёт к OOM.

Альтернативы мемоизации

  • Кеширование на уровне данных (например, результат сетевого запроса сохраняется в сторе).
  • Виртуализация списков (react-window) вместо кеша рендеров для длинных списков.
  • Отложенные/инкрементальные вычисления (партицирование работы) для долгих задач.
  • Web Worker для тяжёлых синхронных вычислений, чтобы не блокировать UI.

Эвристики и ментальные модели

  • Если операция занимает < ~1 ms и вызывается редко — мемоизация не нужна.
  • Если операция дорога и вызывается с одинаковыми входами часто — мемоизация полезна.
  • Сравнивайте: стоимость вычисления × частота вызова vs накладные расходы кеша (память + сравнение аргументов).
  • Для UI: ориентируйтесь на реальные профили производительности (профайлер браузера, React Profiler).

Мaturity levels: этапы внедрения

  1. Осознанность — понимаете проблемы производительности, но не измеряли.
  2. Измерение — собрали профили и нашли узкие места.
  3. Прототип — добавили простую мемоизацию локально и провели тесты.
  4. Production — внедрили, добавили политику очистки кеша и мониторинг.

Мемоизация в React: обзор инструментов

React предлагает три основных инструмента для мемоизации:

  • useMemo — мемоизация значения между рендерами.
  • useCallback — мемоизация функции между рендерами.
  • React.memo — обёртка для компонентов, предотвращающая лишние ререндеры.

useMemo

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

import { useMemo } from 'react';

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Пример компонента:

import React, { useMemo } from 'react';

function App({ value }) {
  const square = (v) => v * v;

  const result = useMemo(() => square(value), [value]);

  return 
{result}
; }

Замечание: useMemo мемоизирует значение, который вы возвращаете из функции, а не саму функцию (в этом поможет useCallback).

React.memo

React.memo — HOC, который предотвращает ререндер функционального компонента, если его пропсы не изменились (поверхностное сравнение).

Пример:

function Comments({ name, comment, likes }) {
  return (
    

{name}

{comment}

{likes}

); } const MemoizedComment = React.memo(Comments); // Использование //

Можно передать кастомную функцию сравнения:

function checkCommentProps(prevProps, nextProps) {
  return (
    prevProps.name === nextProps.name &&
    prevProps.comment === nextProps.comment &&
    prevProps.likes === nextProps.likes
  );
}

const MemoizedComment = React.memo(Comments, checkCommentProps);

Если функция возвращает true — компонент НЕ будет обновлён. Это полезно, когда вы хотите точечно контролировать, какие изменения пропсов должны вызывать ререндер.

Рекомендации:

  • Используйте React.memo для чистых компонентов.
  • Не мемоизируйте всё подряд: сравнения пропсов стоят ресурсов.
  • Оцените сложность props: если у вас сложная вложенная структура, возможно, выгоднее передавать примитивы или мемоизировать данные выше по дереву.

useCallback

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

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Пример с вызовом API:

import React, { useCallback, useEffect } from 'react';

const Component = () => {
  const getData = useCallback(() => {
    console.log('call an API');
    // fetch(...)
  }, []);

  useEffect(() => {
    getData();
  }, [getData]);
};

Если ссылка на getData меняется каждый рендер, useEffect сработает лишний раз. useCallback предотвращает это.

Практическое руководство: когда мемоизировать в React

  • Мемоизируйте вычисления, которые действительно тяжёлые и повторяются.
  • Мемоизируйте функции, если их передают в дочерние компоненты, зависящие от ссылочной идентичности (например, оптимизированные компоненты с React.memo).
  • Не мемоизируйте просто «на всякий случай».
  • Измеряйте: React Profiler и browser devtools помогут принять решение.

Важно: React уже оптимизирован; ненужная мемоизация может ухудшить производительность.

Playbook: шаги для внедрения мемоизации (SOP)

  1. Измерьте проблему: найдите горячую точку через профайлер.
  2. Определите, чистая ли функция и как часто повторяются те же аргументы.
  3. Выберите стратегию (useMemo/useCallback/React.memo/кеш на уровне данных).
  4. Реализуйте мемоизацию локально (в ветке фичи).
  5. Тестируйте: unit и интеграционные тесты должны покрывать поведение.
  6. Измерьте снова: сравните время рендеров и использование памяти.
  7. Внедрите политику очистки кеша (если применимо) и мониторинг.
  8. Документируйте решение и критерии для отката.

Критерии приёмки

  • Производительность компонентов, отмеченных для оптимизации, улучшилась (меньше времени на ререндер по данным профайлера).
  • Нет регрессий в функциональности (тесты проходят).
  • Память под контролем: кеш не растёт бесконтрольно.
  • Документация объясняет причину мемоизации и границы её действия.

Чек-листы по ролям

Разработчик:

  • Измерил проблему в профайлере.
  • Убедился, что функция чистая.
  • Добавил мемоизацию и тесты.
  • Добавил сброс кеша или лимит, если нужно.

Код-ревьювер:

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

Инженер по производительности:

  • Проверил метрики после релиза.
  • Настроил алерты на рост памяти.

Сниппеты / шпаргалка

useMemo (значение):

const memoized = useMemo(() => expensive(a, b), [a, b]);

useCallback (функция):

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

React.memo (компонент):

const MemoComp = React.memo(Component);

Простой кэш с Map и TTL:

function memoizeWithTTL(fn, ttlMs = 60000) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    const entry = cache.get(key);
    const now = Date.now();
    if (entry && now - entry.t < ttlMs) {
      return entry.value;
    }
    const value = fn(...args);
    cache.set(key, { value, t: now });
    return value;
  };
}

Диаграмма принятия решения

flowchart TD
  A[Есть проблема с производительностью?] -->|Нет| B[Не мемоизировать]
  A -->|Да| C[Измерили профайлером]
  C --> D{Чистая функция и одинаковые входы?}
  D -->|Нет| E[Рассмотреть альтернативы: Web Worker/VM/Kafka/перепроектирование данных]
  D -->|Да| F{Функция дорогая/часто вызывается?}
  F -->|Да| G[Использовать мемоизацию: useMemo/useCallback/React.memo]
  F -->|Нет| H[Не оправдано — избегать мемоизации]
  G --> I[Добавить политику очистки/мониторинг]

Факто-бокс: ключевые идеи

  • Мемoизация экономит CPU за счёт памяти.
  • Работает корректно для чистых функций.
  • В React: useMemo — значения, useCallback — функции, React.memo — компоненты.
  • Измеряйте до и после: без данных нельзя точно решить.

Краткий глоссарий в одной строке

  • Чистая функция: зависит только от входных аргументов и не имеет побочных эффектов.
  • Кеш: хранилище для повторно используемых вычисленных значений.
  • TTL: время жизни элемента кэша (time to live).

Частые ошибки и крайние случаи

  • Мемоизировали нечистую функцию (данные устаревают).
  • Забыл политику очистки кеша — рост использования памяти.
  • Мемоизация ради мемоизации: добавленные сравнения дешевле не оказались.
  • Передача изменяемых объектов как ключей без стабилизации ссылок — кеш игнорируется.

Итог

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

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Бесконечная прокрутка на HTML/CSS/JS
Frontend

Бесконечная прокрутка на HTML/CSS/JS

Как измерить потребление электроэнергии ПК — методы и расчёты
Hardware

Как измерить потребление электроэнергии ПК — методы и расчёты

React Native Elements: быстрый старт и темизация
Разработка мобильных приложений

React Native Elements: быстрый старт и темизация

Тёмная тема в Vue — CSS-переменные и LocalStorage
Frontend

Тёмная тема в Vue — CSS-переменные и LocalStorage

Развёртывание React на GitHub Pages
Dev

Развёртывание React на GitHub Pages

Как посмотреть историю дружбы в Facebook
Социальные сети

Как посмотреть историю дружбы в Facebook