Skip to main content
Практика завдань

Мемоізація в JavaScript

Що таке Мемоізація?

Мемоізація — це техніка оптимізації, яка зберігає (кешує) результати виконання функцій для конкретних аргументів. При наступних викликах з тими ж аргументами функція повертає кешований результат замість повторного обчислення.

Простими словами

Мемоізація — це "запам'ятовування" результатів дорогих обчислень, щоб не доводилося їх повторювати.


Чому потрібна Мемоізація?

  • Оптимізація продуктивності дорогих обчислень
  • Зменшення повторних обчислень
  • Прискорення рекурсивних алгоритмів
  • Оптимізація рендерингу в React

Основна реалізація

javascript
function memoize(fn) { const cache = {}; // Об'єкт для зберігання результатів return function(...args) { const key = JSON.stringify(args); // Ключ з аргументів if (key in cache) { console.log('З кешу'); return cache[key]; } console.log('Обчислення'); const result = fn(...args); cache[key] = result; return result; }; } // Використання function expensiveSum(a, b) { // Симуляція тривалої операції for (let i = 0; i < 1000000000; i++) {} return a + b; } const memoizedSum = memoize(expensiveSum); console.time('Перший виклик'); memoizedSum(5, 10); // Обчислення console.timeEnd('Перший виклик'); // ~1000ms console.time('Другий виклик'); memoizedSum(5, 10); // З кешу console.timeEnd('Другий виклик'); // ~0ms

Класичний приклад: Числа Фібоначчі

Без Мемоізації (дуже повільно)

javascript
function fibonacci(n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); } console.time('fib'); fibonacci(40); // ~1.5 секунди console.timeEnd('fib');

Проблема: Функція обчислює одні й ті ж значення багато разів.

fib(5)

fib(4)

fib(3)

fib(3)

fib(2)

fib(2)

fib(2)

fib(3) обчислено 2 рази, fib(2) — 3 рази!

З Мемоізацією (швидко)

javascript
const fibonacci = memoize((n) => { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); }); console.time('fib'); fibonacci(40); // ~0.5 мілісекунд console.timeEnd('fib');

Результат:

Швидкість зростає в тисячі разів для великих значень N.


Розширена реалізація

З обмеженням на розмір кешу

javascript
function memoize(fn, maxSize = 100) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = fn(...args); // Обмеження розміру кешу if (cache.size >= maxSize) { const firstKey = cache.keys().next().value; cache.delete(firstKey); // Видалити найстаріший } cache.set(key, result); return result; }; }

З TTL (Часом життя)

javascript
function memoizeWithTTL(fn, ttl = 5000) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < ttl) { return cached.value; } const result = fn(...args); cache.set(key, { value: result, timestamp: Date.now() }); return result; }; } // Використання const fetchUser = memoizeWithTTL(async (id) => { const response = await fetch(`/api/users/${id}`); return response.json(); }, 10000); // Кеш на 10 секунд

З LRU (Найменш нещодавно використаний)

javascript
function memoizeLRU(fn, maxSize = 100) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) { const value = cache.get(key); // Перемістити в кінець (позначити як нещодавно використаний) cache.delete(key); cache.set(key, value); return value; } const result = fn(...args); cache.set(key, result); // Видалити найстаріший (перший) елемент if (cache.size > maxSize) { const firstKey = cache.keys().next().value; cache.delete(firstKey); } return result; }; }

Мемоізація в React

React.memo

Мемоізує весь компонент — пропускає повторний рендер, якщо пропси не змінилися.

javascript
import { memo } from 'react'; const ExpensiveComponent = memo(({ data }) => { console.log('Рендеринг ExpensiveComponent'); // Важкі обчислення const processed = processData(data); return <div>{processed}</div>; }); // Компонент повторно рендериться лише якщо дані змінилися

З користувацьким порівнянням

javascript
const UserCard = memo( ({ user }) => { return <div>{user.name}</div>; }, (prevProps, nextProps) => { // Повертає true, якщо НЕ потрібно оновлювати return prevProps.user.id === nextProps.user.id; } );

useMemo

Мемоізує результат обчислення всередині компонент.

javascript
import { useMemo } from 'react'; function ProductList({ products, filterText }) { const filteredProducts = useMemo(() => { console.log('Фільтрація продуктів'); return products.filter(p => p.name.toLowerCase().includes(filterText.toLowerCase()) ); }, [products, filterText]); return ( <ul> {filteredProducts.map(p => ( <li key={p.id}>{p.name}</li> ))} </ul> ); }

Важливо:

useMemo перераховує значення лише тоді, коли залежності в масиві [products, filterText] змінюються.

useCallback

Мемоізує саму функцію (щоб уникнути створення нової функції при кожному рендері).

javascript
import { useCallback, memo } from 'react'; const Button = memo(({ onClick, children }) => { console.log(`Рендеринг кнопки "${children}"`); return <button onClick={onClick}>{children}</button>; }); function Parent() { const [count, setCount] = useState(0); const [other, setOther] = useState(0); // Створює нову функцію при кожному рендері const handleClick = () => setCount(count + 1); // Функція створюється один раз const handleClickMemo = useCallback(() => { setCount(prev => prev + 1); }, []); return ( <> <Button onClick={handleClickMemo}>Кількість: {count}</Button> <button onClick={() => setOther(other + 1)}>Інше: {other}</button> </> ); }

Коли НЕ використовувати Мемоізацію?

Для простих обчислень

javascript
// Не потрібно const doubled = useMemo(() => count * 2, [count]); // Краще просто const doubled = count * 2;

Для функцій з побічними ефектами

javascript
// Погано - виклик API буде кешуватися назавжди const fetchData = memoize(async (id) => { return await fetch(`/api/users/${id}`); });

Коли кеш займає більше пам'яті, ніж економить часу

javascript
// Якщо функція викликається рідко з різними аргументами const memoizedSort = memoize(arr => [...arr].sort()); // Кеш буде рости безмежно

Бібліотеки для Мемоізації

Lodash

javascript
import { memoize } from 'lodash'; const expensiveFn = memoize((a, b) => { return a + b; }); // Можна встановити користувацький резолвер для ключа const memoized = memoize( (obj) => obj.value, (obj) => obj.id // Ключ кешу );

fast-memoize

javascript
import memoize from 'fast-memoize'; const fn = memoize((a, b) => a + b);

reselect (для Redux)

javascript
import { createSelector } from 'reselect'; const getUsers = state => state.users; const getFilter = state => state.filter; const getFilteredUsers = createSelector( [getUsers, getFilter], (users, filter) => users.filter(u => u.name.includes(filter)) ); // Результат кешується

Підводні камені

1. Сериалізація аргументів

javascript
// Об'єкти з однаковими даними, але різними посиланнями const obj1 = { id: 1 }; const obj2 = { id: 1 }; JSON.stringify(obj1) === JSON.stringify(obj2); // true, але повільно // Краще використовувати примітиви як ключі function memoize(fn) { const cache = new Map(); return function(id) { // Тільки примітивний аргумент if (cache.has(id)) return cache.get(id); const result = fn(id); cache.set(id, result); return result; }; }

2. Витоки пам'яті

javascript
// Кеш росте безмежно const memoized = memoize(expensiveFn); // Обмежити розмір або додати TTL const memoized = memoizeLRU(expensiveFn, 100);

3. Мутації в React

javascript
// useMemo не спрацює з мутацією об'єкта const obj = { count: 0 }; obj.count++; // Мутація - те саме посилання // Створити новий об'єкт setObj({ ...obj, count: obj.count + 1 });

Висновок

Мемоізація:

  • Оптимізує дорогі обчислення
  • Критично важлива для рекурсивних алгоритмів
  • Важлива для продуктивності додатків на React
  • Не завжди потрібна — вимірюйте продуктивність
  • Може призвести до витоків пам'яті без контролю розміру кешу
  • Вимагає належного управління залежностями

На співбесідах:

Важливо вміти:

  • Пояснити, що таке мемоізація і чому вона потрібна
  • Реалізувати базову функцію memoize
  • Навести приклади (Фібоначчі, запити API)
  • Пояснити різницю між useMemo, useCallback та React.memo
  • Розуміти, коли мемоізація не потрібна або шкідлива

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?
Практика завдань