Practice JS Problems

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 memoize function
  • Provide examples (Fibonacci, API requests)
  • Explain difference between useMemo, useCallback, and React.memo
  • Understand when memoization is not needed or harmful
Practice JS Problems