Suggest an editImprove this articleRefine the answer for “What is memoization?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Memoization** is a performance optimization where a function caches its result for given inputs and returns the cached value on repeat calls instead of recomputing. ```javascript const memoize = (fn) => { const cache = {}; return (...args) => { const key = JSON.stringify(args); return cache[key] ?? (cache[key] = fn(...args)); }; }; ``` **Key point:** works only on pure functions; if external state changes between calls, the cached result becomes stale.Shown above the full answer for quick recall.Answer (EN)Image**Memoization** is a performance optimization where a function caches its return value for a given set of inputs, so the next call with the same inputs skips the computation and returns the cached result directly. ## Theory ### TL;DR - Think of a chef writing cook times on a notepad: same order comes in, grab the note instead of cooking from scratch - Works only on pure functions; if a function reads `Date.now()` or a global counter, the cache returns stale results - Use it when the same function runs repeatedly with identical args and each call costs more than ~1ms - In React, `useMemo` and `useCallback` are the built-in tools for this pattern ### Quick example ```javascript // Without memoization: fib(40) triggers ~2 billion recursive calls function fib(n) { if (n < 2) return n; return fib(n - 1) + fib(n - 2); } // With memoization: each n is computed once, rest from cache const memoize = (fn) => { const cache = {}; return (...args) => { const key = JSON.stringify(args); return cache[key] ?? (cache[key] = fn(...args)); }; }; const fastFib = memoize(fib); fastFib(40); // Computes once fastFib(40); // Returns from cache, no recomputation ``` The cache is a plain object keyed by serialized arguments. On a cache hit, the function body never runs. ### Pure functions only Memoization works because the same input always produces the same output. That assumption breaks the moment a function reads external state. `Math.random()`, `Date.now()`, a database query - any of these make the cached result stale immediately. The cache stores the first answer forever, even as everything around it changes. In practice, the sneakiest version of this bug is a function that reads a module-level config variable. It looks pure because the arguments never vary, but the output depends on something external. In React, this surfaces with object props. Pass a new object reference on every render and `useMemo` recomputes every time, even if the data inside is identical. React compares the deps array with shallow `Object.is`, so `{a: 1}` and `{a: 1}` are different objects unless they are the same reference. ### When to use - Recursive algorithms with overlapping calls like Fibonacci: `fib(40)` drops from O(2^n) to O(n) - React components that filter or transform large lists on every render - Functions called repeatedly with the same IDs, such as per-user config lookups - Skip it for one-time calls, functions with side effects, or operations under ~1ms ### How it works internally The basic implementation is a closure over a plain object or `Map`. On each call, arguments are stringified into a cache key and checked for a hit. React's `useMemo` stores the cached value in the fiber node and on every render compares the deps array with `Object.is`. If deps match, it skips the computation and returns the stored value without running the function body. ### Common mistakes **Memoizing an impure function** ```javascript const getRandom = memoize(() => Math.random()); getRandom(); // 0.42 getRandom(); // Still 0.42 — frozen at the first call ``` No inputs means no key differentiation. The first result is cached and never expires. **Stale deps in React** ```javascript // BUG: empty deps closes over the initial value of items const result = useMemo(() => heavyCalc(items), []); // Fix: list actual dependencies const result = useMemo(() => heavyCalc(items), [items]); ``` This is the most common interview mistake. The `exhaustive-deps` ESLint rule catches it automatically. **Over-memoizing cheap operations** ```javascript // Not worth it: V8 optimizes this loop already const doubled = useMemo(() => arr.map(x => x * 2), [arr]); ``` Memoization adds its own overhead: key generation, cache lookup, memory allocation. For operations under ~1ms, the overhead often exceeds the savings. Profile before you memoize. **Mutating a cached object** ```javascript function getData(id) { if (!cache[id]) cache[id] = fetchSync(id); cache[id].tags.push('new-tag'); // Corrupts every caller sharing this cache entry } ``` The cache holds a reference, not a copy. Any code pointing at that cached object sees the mutation. ### Real-world usage - React 18: `useMemo` for filtered lists, `useCallback` to stabilize event handler references passed to children - Lodash: `_.memoize` for utility functions shared across many components - Redux Toolkit: `createSelector` via Reselect for derived state like todos filtered by userId - Next.js 14: `unstable_cache` for server components fetching the same data across requests - Node.js/Express: middleware caching JSON responses for read-heavy endpoints like `/api/users/:id` ### Follow-up questions **Q:** What is the difference between memoization and caching? **A:** Memoization is function-scoped and lives in memory for the lifetime of the closure or program. General caching is app-wide, often stored in Redis or another external store, with explicit expiry policies. **Q:** When does `useMemo` not prevent a re-render? **A:** When the parent passes a new object reference as a prop on every render, even if the data inside is the same. The deps comparison is shallow, so two separate `{a: 1}` objects are not equal. You need `useCallback` upstream to stabilize the reference first. **Q:** What is the space tradeoff with memoization? **A:** The cache grows with every unique input combination. For functions with many distinct inputs, memory usage can become significant. A common fix is TTL-based eviction: store `{value, expiry}` in the cache and recompute when `Date.now()` exceeds the expiry. **Q:** Why memoize `fib` if V8 already optimizes JavaScript execution? **A:** JavaScript has no tail call optimization in practice. Without memoization, `fib(40)` makes roughly 2^40 recursive calls. With memoization, each value from 0 to 40 is computed exactly once. ## Examples ### Basic: memoize wrapper ```javascript function memoize(fn) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn(...args); cache.set(key, result); return result; }; } function slowSquare(n) { // Imagine this computation takes 50ms return n * n; } const fastSquare = memoize(slowSquare); fastSquare(4); // 16, computed fastSquare(4); // 16, from cache fastSquare(9); // 81, computed (new input) ``` The wrapper intercepts every call, checks the cache first, and only runs the original function on a miss. A `Map` handles key lookups more reliably than a plain object for non-string inputs. ### Intermediate: React todo list with useMemo A list of 1000 todos should not re-filter on every keystroke in an unrelated search input. ```javascript import { useMemo, useState } from 'react'; function TodoList({ todos }) { const [filter, setFilter] = useState('all'); const [searchText, setSearchText] = useState(''); // Without useMemo: filters 1000 items on every render, including searchText changes const visibleTodos = useMemo(() => todos.filter(todo => { if (filter === 'all') return true; return filter === 'done' ? todo.done : !todo.done; }), [todos, filter] // Recomputes only when todos or filter changes, not searchText ); return ( <> <input value={searchText} onChange={e => setSearchText(e.target.value)} placeholder="Search..." /> <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul> </> ); } ``` Typing in the search input triggers re-renders, but `visibleTodos` reads from cache every time. The filter only re-runs when `todos` or `filter` actually changes.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.