Memoization in JavaScript

Memoization is a powerful optimization technique in JavaScript that boosts performance by caching the results of expensive function calls and reusing them when the same inputs occur again. By avoiding redundant computations, memoization is ideal for recursive algorithms, complex calculations, or functions called repeatedly with identical inputs. In this blog, we’ll explore what memoization is, how it works, and demonstrate its power with practical examples. Let’s dive into making your JavaScript code faster and smarter!


What is Memoization?

Memoization is a technique that stores the results of costly function calls in a cache, retrieving them when the same inputs are provided again. This trade-off—using memory to store results in exchange for faster execution—is particularly effective for pure functions (functions that produce the same output for the same input without side effects) and recursive algorithms with overlapping subproblems.

Key Benefits

  • Performance: Eliminates redundant calculations, reducing time complexity for expensive operations.

  • Use Cases: Optimizes recursive functions (e.g., Fibonacci, factorial), API calls, or dynamic programming problems.

  • Trade-Off: Increases memory usage due to caching, which requires careful management for large input sets.


How Memoization Works

Memoization involves four steps:

  1. Initialize a Cache: Use an object, array, or Map to store input-output pairs.

  2. Check the Cache: Before computing, verify if the result for the given input is already cached.

  3. Compute and Store: If the result isn’t cached, calculate it, store it in the cache, and return it.

  4. Return Cached Result: If the result exists in the cache, return it directly, skipping computation.

    Memoization flowchart

In JavaScript, memoization is often implemented using closures within higher-order functions, where an outer function creates a cache and returns an inner function that uses it. This ensures the cache persists across calls, as we’ll see in the examples.


Practical Examples of Memoization

Let’s explore examples that showcase memoization in action, from optimizing recursive algorithms to creating reusable memoizers.

Example 1: Memoized Fibonacci Function

The Fibonacci sequence, where each number is the sum of the two preceding ones, is computationally expensive due to redundant recursive calls. Memoization drastically improves its performance.

// Non-memoized Fibonacci (slow for large n)
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

// Memoized Fibonacci
function memoizedFib() {
  const cache = {};
  return function fib(n) {
    if (n in cache) return cache[n]; // Return cached result
    if (n <= 1) return n;
    cache[n] = fib(n - 1) + fib(n - 2); // Compute and cache
    return cache[n];
  };
}

const fibFast = memoizedFib();
console.log(fibFast(40)); // 102334155 (fast)
console.log(fibFast(40)); // 102334155 (cached, instant)
console.log(fibFast(41)); // fib(41) needs fib(40) + fib(39), both already cached!

How It Works:

  • Non-Memoized: The basic fib function recalculates values (e.g., fib(39)) multiple times, leading to exponential time complexity (O(2^n)).

  • Memoized: memoizedFib creates a closure over a cache object. The inner fib function checks cache before computing. For fibFast(40), it caches intermediate results (e.g., cache[39], cache[38]), reducing complexity to O(n).

  • Closure and Higher-Order Function: memoizedFib is a higher-order function that returns fib, which uses a closure to maintain cache. This ensures all calls to fibFast share the same cache, enabling instant retrieval for repeated inputs.

  • Output: The first call computes and caches results; the second call retrieves cache[40] instantly.

Example 2: Generic Memoization Function

A generic memoizer wraps any function to add memoization, handling multiple arguments by serializing them into a cache key.

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args); // Serialize arguments as cache key
    if (key in cache) return cache[key];
    cache[key] = fn(...args);
    return cache[key];
  };
}

// Example: Expensive computation
function expensiveCalculation(a, b) {
  console.log(`Computing ${a} + ${b}...`);
  return a + b; // Simulating an expensive operation
}

const memoizedCalc = memoize(expensiveCalculation);
console.log(memoizedCalc(2, 3)); // Computing 2 + 3 ... 5
console.log(memoizedCalc(2, 3)); // 5 (cached, no recomputation)
console.log(memoizedCalc(4, 5)); // Computing 4 + 5 ... 9

How It Works:

  • Generic Memoizer: memoize is a higher-order function that takes any function fn and returns a memoized version, using a closure to maintain cache.

  • Cache Key: JSON.stringify(args) creates a unique key for argument combinations (e.g., [2, 3] becomes "[2,3]").

  • Behavior: The first memoizedCalc(2, 3) computes and caches 5. The second call retrieves it from cache. A call with 4, 5 computes and caches 9.

  • Use Case: Versatile for any pure function, such as API responses or complex calculations.


Key Considerations

  • Memory Usage: Caching consumes memory, especially for functions with many unique inputs. Consider cache eviction strategies (e.g., limiting cache size) for large datasets.

  • Pure Functions: Memoization is most effective for pure functions, as side effects can lead to inconsistent cached results.

  • Argument Serialization: Using JSON.stringify for cache keys may fail with non-serializable inputs (e.g., functions, circular objects). Custom key generation may be needed.

  • Thread Safety: JavaScript’s single-threaded nature eliminates thread safety concerns, but ensure the cache is scoped properly using closures.


Practical Applications

  1. Recursive Algorithms: Optimize functions like Fibonacci or factorial by caching intermediate results.

  2. API Calls: Cache responses for repeated API requests with identical parameters to reduce network overhead.

  3. Dynamic Programming: Solve problems with overlapping subproblems, such as shortest path algorithms.

  4. UI Rendering: Cache computed values in web applications to enhance rendering performance.


Thank you for reading!
Try memoization in your projects to unlock significant speed improvements, and share your favourite applications in the comments.
Happy coding! 🚀

0
Subscribe to my newsletter

Read articles from Deepthi Purijala directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Deepthi Purijala
Deepthi Purijala

Full Stack Developer with hands-on experience of more than 1 year. Proficient in both Back-end and Front-end technologies, with a strong commitment to delivering high-quality code