Memoization in JavaScript
What is Memoization?
Memoization is an optimization technique that stores (caches) the results of function execution for specific arguments. On subsequent calls with the same arguments, the function returns the cached result instead of recomputing.
In Simple Terms
Memoization is "remembering" the results of expensive computations so you don't have to redo them.
Why is Memoization Needed?
- Optimize performance of expensive computations
- Reduce repeated calculations
- Speed up recursive algorithms
- Optimize rendering in React
Basic Implementation
function memoize(fn) {
const cache = {}; // Object to store results
return function(...args) {
const key = JSON.stringify(args); // Key from arguments
if (key in cache) {
console.log('From cache');
return cache[key];
}
console.log('Computing');
const result = fn(...args);
cache[key] = result;
return result;
};
}
// Usage
function expensiveSum(a, b) {
// Simulate long operation
for (let i = 0; i < 1000000000; i++) {}
return a + b;
}
const memoizedSum = memoize(expensiveSum);
console.time('First call');
memoizedSum(5, 10); // Computing
console.timeEnd('First call'); // ~1000ms
console.time('Second call');
memoizedSum(5, 10); // From cache
console.timeEnd('Second call'); // ~0ms
Classic Example: Fibonacci Numbers
Without Memoization (very slow)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.time('fib');
fibonacci(40); // ~1.5 seconds
console.timeEnd('fib');
Problem: Function computes the same values many times.
fib(3) computed 2 times, fib(2) — 3 times!
With Memoization (fast)
const fibonacci = memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.time('fib');
fibonacci(40); // ~0.5 milliseconds
console.timeEnd('fib');
Result:
Speed increases by thousands of times for large N values.
Advanced Implementation
With Cache Size Limit
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);
// Limit cache size
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey); // Remove oldest
}
cache.set(key, result);
return result;
};
}
With TTL (Time To Live)
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;
};
}
// Usage
const fetchUser = memoizeWithTTL(async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
}, 10000); // Cache for 10 seconds
With LRU (Least Recently Used)
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);
// Move to end (mark as recently used)
cache.delete(key);
cache.set(key, value);
return value;
}
const result = fn(...args);
cache.set(key, result);
// Remove oldest (first) element
if (cache.size > maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
return result;
};
}
Memoization in React
React.memo
Memoizes entire component — skips re-render if props haven't changed.
import { memo } from 'react';
const ExpensiveComponent = memo(({ data }) => {
console.log('Rendering ExpensiveComponent');
// Heavy computations
const processed = processData(data);
return <div>{processed}</div>;
});
// Component re-renders only if data changed
With Custom Comparison
const UserCard = memo(
({ user }) => {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Return true if should NOT update
return prevProps.user.id === nextProps.user.id;
}
);
useMemo
Memoizes computation result inside component.
import { useMemo } from 'react';
function ProductList({ products, filterText }) {
const filteredProducts = useMemo(() => {
console.log('Filtering products');
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>
);
}
Important:
useMemo recalculates value only when dependencies in array [products, filterText] change.
useCallback
Memoizes the function itself (to avoid creating new function on each render).
import { useCallback, memo } from 'react';
const Button = memo(({ onClick, children }) => {
console.log(`Rendering button "${children}"`);
return <button onClick={onClick}>{children}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// Creates new function on each render
const handleClick = () => setCount(count + 1);
// Function created once
const handleClickMemo = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<>
<Button onClick={handleClickMemo}>Count: {count}</Button>
<button onClick={() => setOther(other + 1)}>Other: {other}</button>
</>
);
}
When NOT to Use Memoization?
For Simple Computations
// Not needed
const doubled = useMemo(() => count * 2, [count]);
// Better just
const doubled = count * 2;
For Functions with Side Effects
// Bad - API call will be cached forever
const fetchData = memoize(async (id) => {
return await fetch(`/api/users/${id}`);
});
When Cache Takes More Memory Than It Saves Time
// If function called rarely with different arguments
const memoizedSort = memoize(arr => [...arr].sort());
// Cache will grow infinitely
Libraries for Memoization
Lodash
import { memoize } from 'lodash';
const expensiveFn = memoize((a, b) => {
return a + b;
});
// Can set custom resolver for key
const memoized = memoize(
(obj) => obj.value,
(obj) => obj.id // Cache key
);
fast-memoize
import memoize from 'fast-memoize';
const fn = memoize((a, b) => a + b);
reselect (for 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))
);
// Result is cached
Pitfalls
1. Argument Serialization
// Objects with same data but different references
const obj1 = { id: 1 };
const obj2 = { id: 1 };
JSON.stringify(obj1) === JSON.stringify(obj2); // true, but slow
// Better to use primitives as keys
function memoize(fn) {
const cache = new Map();
return function(id) { // Only primitive argument
if (cache.has(id)) return cache.get(id);
const result = fn(id);
cache.set(id, result);
return result;
};
}
2. Memory Leaks
// Cache grows infinitely
const memoized = memoize(expensiveFn);
// Limit size or add TTL
const memoized = memoizeLRU(expensiveFn, 100);
3. Mutations in React
// useMemo won't work with object mutation
const obj = { count: 0 };
obj.count++; // Mutation - same reference
// Create new object
setObj({ ...obj, count: obj.count + 1 });
Conclusion
Memoization:
- Optimizes expensive computations
- Critical for recursive algorithms
- Important for React application performance
- Not always needed — measure performance
- Can lead to memory leaks without cache size control
- Requires proper dependency management
In Interviews:
Important to be able to:
- Explain what memoization is and why it's needed
- Implement basic
memoizefunction - Provide examples (Fibonacci, API requests)
- Explain difference between
useMemo,useCallback, andReact.memo - Understand when memoization is not needed or harmful