Мемоізація в JavaScript
Що таке Мемоізація?
Мемоізація — це техніка оптимізації, яка зберігає (кешує) результати виконання функцій для конкретних аргументів. При наступних викликах з тими ж аргументами функція повертає кешований результат замість повторного обчислення.
Простими словами
Мемоізація — це "запам'ятовування" результатів дорогих обчислень, щоб не доводилося їх повторювати.
Чому потрібна Мемоізація?
- Оптимізація продуктивності дорогих обчислень
- Зменшення повторних обчислень
- Прискорення рекурсивних алгоритмів
- Оптимізація рендерингу в React
Основна реалізація
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Класичний приклад: Числа Фібоначчі
Без Мемоізації (дуже повільно)
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 рази!
З Мемоізацією (швидко)
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.
Розширена реалізація
З обмеженням на розмір кешу
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 (Часом життя)
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 (Найменш нещодавно використаний)
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
Мемоізує весь компонент — пропускає повторний рендер, якщо пропси не змінилися.
import { memo } from 'react';
const ExpensiveComponent = memo(({ data }) => {
console.log('Рендеринг ExpensiveComponent');
// Важкі обчислення
const processed = processData(data);
return <div>{processed}</div>;
});
// Компонент повторно рендериться лише якщо дані змінилисяЗ користувацьким порівнянням
const UserCard = memo(
({ user }) => {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Повертає true, якщо НЕ потрібно оновлювати
return prevProps.user.id === nextProps.user.id;
}
);useMemo
Мемоізує результат обчислення всередині компонент.
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
Мемоізує саму функцію (щоб уникнути створення нової функції при кожному рендері).
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>
</>
);
}Коли НЕ використовувати Мемоізацію?
Для простих обчислень
// Не потрібно
const doubled = useMemo(() => count * 2, [count]);
// Краще просто
const doubled = count * 2;Для функцій з побічними ефектами
// Погано - виклик API буде кешуватися назавжди
const fetchData = memoize(async (id) => {
return await fetch(`/api/users/${id}`);
});Коли кеш займає більше пам'яті, ніж економить часу
// Якщо функція викликається рідко з різними аргументами
const memoizedSort = memoize(arr => [...arr].sort());
// Кеш буде рости безмежноБібліотеки для Мемоізації
Lodash
import { memoize } from 'lodash';
const expensiveFn = memoize((a, b) => {
return a + b;
});
// Можна встановити користувацький резолвер для ключа
const memoized = memoize(
(obj) => obj.value,
(obj) => obj.id // Ключ кешу
);fast-memoize
import memoize from 'fast-memoize';
const fn = memoize((a, b) => a + b);reselect (для Redux)
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. Сериалізація аргументів
// Об'єкти з однаковими даними, але різними посиланнями
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. Витоки пам'яті
// Кеш росте безмежно
const memoized = memoize(expensiveFn);
// Обмежити розмір або додати TTL
const memoized = memoizeLRU(expensiveFn, 100);3. Мутації в React
// useMemo не спрацює з мутацією об'єкта
const obj = { count: 0 };
obj.count++; // Мутація - те саме посилання
// Створити новий об'єкт
setObj({ ...obj, count: obj.count + 1 });Висновок
Мемоізація:
- Оптимізує дорогі обчислення
- Критично важлива для рекурсивних алгоритмів
- Важлива для продуктивності додатків на React
- Не завжди потрібна — вимірюйте продуктивність
- Може призвести до витоків пам'яті без контролю розміру кешу
- Вимагає належного управління залежностями
На співбесідах:
Важливо вміти:
- Пояснити, що таке мемоізація і чому вона потрібна
- Реалізувати базову функцію
memoize - Навести приклади (Фібоначчі, запити API)
- Пояснити різницю між
useMemo,useCallbackтаReact.memo - Розуміти, коли мемоізація не потрібна або шкідлива
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.