Skip to main content

Command Palette

Search for a command to run...

JavaScript Memoization: Cache Me If You Can

Updated
7 min read
JavaScript Memoization: Cache Me If You Can
B

I'm a full stack developer from Ghana. I'm passionate about helping others gain their ground in tech, specifically web development.

Ever watched your JavaScript application grind to a halt while recalculating the same expensive operation for the thousandth time? It's like watching someone manually count change instead of using a calculator—technically correct, but painfully inefficient.

Enter memoization (not memorization—that's what you did in school with multiplication tables). This elegant optimization technique is like giving your functions a perfect memory, allowing them to remember and instantly recall previous calculations instead of doing the math all over again.

Think of memoization as your code's personal assistant who never forgets anything and always has the answer ready before you finish asking the question.

What Is Memoization, Really?

Memoization is a caching technique where you store the results of expensive function calls and return the cached result when the same inputs occur again. It's the programming equivalent of writing down the answer to a complex math problem so you don't have to solve it again later.

Here's the basic concept in action:

// Without memoization: "What's 2 + 2?" *calculates* "It's 4!"
// With memoization: "What's 2 + 2?" *checks notes* "I already know this—it's 4!"

Why Should You Care?

1. Speed That Actually Matters

Imagine you're building an e-commerce app that calculates shipping costs based on weight, distance, and delivery options. Without memoization:

// This runs every time someone views a product
function calculateShipping(weight, distance, options) {
  // Complex calculation involving API calls, tax lookups, etc.
  // Takes 200ms each time
  return expensiveShippingCalculation(weight, distance, options);
}

// User browses 20 products = 4 seconds of waiting
// User refreshes page = another 4 seconds
// Multiple users = your server crying

With memoization, the second request for the same shipping calculation is instant. Your users stay happy, your server stays cool.

2. Memory vs. CPU Trade-offs

Memoization is essentially trading memory for speed. In most modern applications, memory is cheaper than computation time, making this a fantastic trade-off. Your users would rather you use a few extra MB of RAM than make them wait.

3. Recursive Algorithm Salvation

Some algorithms are naturally recursive and compute the same subproblems repeatedly. Memoization transforms these exponential-time disasters into efficient, linear solutions.

Real-World Memoization Examples

Let's look at scenarios you might actually encounter.

Example 1: API Response Caching

const memoize = (fn) => {
  const cache = new Map();

  return (...args) => {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      console.log('Cache hit! 🎯');
      return cache.get(key);
    }

    console.log('Computing result... ⏳');
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
};

// Expensive API call that we don't want to repeat
const fetchUserProfile = memoize(async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  const userData = await response.json();

  // Simulate expensive processing
  await new Promise(resolve => setTimeout(resolve, 1000));

  return {
    ...userData,
    displayName: `${userData.firstName} ${userData.lastName}`,
    avatar: userData.avatar || '/default-avatar.png'
  };
});

// First call: hits the API and takes ~1 second
await fetchUserProfile(123);

// Second call: instant response from cache
await fetchUserProfile(123); // Cache hit! 🎯

Example 2: Complex Calculations with Multiple Parameters

const memoizedPriceCalculator = memoize((basePrice, taxRate, discounts, shipping) => {
  // Simulate complex pricing logic
  let finalPrice = basePrice;

  // Apply discounts
  discounts.forEach(discount => {
    if (discount.type === 'percentage') {
      finalPrice *= (1 - discount.value / 100);
    } else {
      finalPrice -= discount.value;
    }
  });

  // Add tax
  finalPrice *= (1 + taxRate);

  // Add shipping
  finalPrice += shipping;

  return Math.round(finalPrice * 100) / 100; // Round to 2 decimal places
});

// These calls are expensive without memoization
console.log(memoizedPriceCalculator(99.99, 0.08, [{type: 'percentage', value: 10}], 5.99));
console.log(memoizedPriceCalculator(99.99, 0.08, [{type: 'percentage', value: 10}], 5.99)); // Instant!

Example 3: DOM Query Optimization

const memoizedQuerySelector = memoize((selector) => {
  console.log(`Searching DOM for: ${selector}`);
  return document.querySelectorAll(selector);
});

// Instead of repeatedly querying the DOM
function highlightElements(className) {
  // First call: searches the DOM
  const elements = memoizedQuerySelector(`.${className}`);

  elements.forEach(el => el.classList.add('highlighted'));

  // Subsequent calls with same className: instant
  const sameElements = memoizedQuerySelector(`.${className}`);
  console.log('Found', sameElements.length, 'elements'); // No DOM search!
}

Advanced Memoization Patterns

Time-Based Cache Invalidation

Sometimes cached data gets stale. Here's a memoization function with TTL (Time To Live):

const memoizeWithTTL = (fn, ttlMs = 60000) => {
  const cache = new Map();

  return (...args) => {
    const key = JSON.stringify(args);
    const cached = cache.get(key);

    if (cached && Date.now() - cached.timestamp < ttlMs) {
      return cached.value;
    }

    const result = fn(...args);
    cache.set(key, {
      value: result,
      timestamp: Date.now()
    });

    return result;
  };
};

// Cache API responses for 5 minutes
const fetchWeatherData = memoizeWithTTL(async (city) => {
  const response = await fetch(`/api/weather/${city}`);
  return response.json();
}, 5 * 60 * 1000);

LRU (Least Recently Used) Cache

For memory-conscious applications, implement a cache that automatically removes old entries:

const memoizeWithLRU = (fn, maxSize = 100) => {
  const cache = new Map();

  return (...args) => {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      // Move to end (most recently used)
      const value = cache.get(key);
      cache.delete(key);
      cache.set(key, value);
      return value;
    }

    const result = fn(...args);

    // Remove oldest if at capacity
    if (cache.size >= maxSize) {
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }

    cache.set(key, result);
    return result;
  };
};

When NOT to Use Memoization

Memoization isn't a magic bullet. Avoid it when:

1. Functions Have Side Effects

// DON'T memoize this!
const updateUserCount = memoize((increment) => {
  userCount += increment; // Side effect!
  return userCount;
});

2. Inputs Are Always Unique

// Pointless memoization - timestamps are always different
const logWithTimestamp = memoize((message) => {
  return `${Date.now()}: ${message}`;
});

3. Memory Is More Expensive Than Computation

// If your function returns huge objects but is cheap to compute
const generateLargeArray = memoize(() => {
  return new Array(1000000).fill(0); // Wastes memory for a simple operation
});

4. Functions Are Rarely Called With Same Arguments If your function is rarely called with the same inputs, memoization just adds overhead without benefits.

Modern Alternatives and Libraries

React's useMemo and useCallback

React developers get memoization built-in:

import React, { useMemo, useCallback } from 'react';

function ExpensiveComponent({ data, filter }) {
  // Memoize expensive calculations
  const processedData = useMemo(() => {
    return data.filter(filter).map(item => expensiveTransformation(item));
  }, [data, filter]);

  // Memoize callback functions
  const handleClick = useCallback((id) => {
    onItemClick(id);
  }, [onItemClick]);

  return <div>{/* render processedData */}</div>;
}
  • Lodash: _.memoize(fn) with customizable cache

  • Memoizee: Advanced memoization with TTL, LRU, and more

  • Fast-memoize: Optimized for performance

Performance Tips and Best Practices

1. Choose Your Cache Keys Wisely

// Good: Simple, consistent key generation
const memoizedFn = memoize((a, b, c) => {
  // Function uses JSON.stringify([a, b, c]) as key
});

// Better: Custom key function for complex objects
const memoizeWithCustomKey = (fn, keyFn) => {
  const cache = new Map();
  return (...args) => {
    const key = keyFn ? keyFn(...args) : JSON.stringify(args);
    // ... rest of memoization logic
  };
};

2. Monitor Cache Hit Rates

const createInstrumentedMemoize = (fn) => {
  let hits = 0;
  let misses = 0;
  const cache = new Map();

  const memoizedFn = (...args) => {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      hits++;
      return cache.get(key);
    }

    misses++;
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };

  memoizedFn.stats = () => ({ hits, misses, hitRate: hits / (hits + misses) });
  memoizedFn.clearCache = () => cache.clear();

  return memoizedFn;
};

3. Handle Async Functions Properly

const memoizeAsync = (fn) => {
  const cache = new Map();

  return async (...args) => {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    // Cache the promise, not just the resolved value
    const promise = fn(...args);
    cache.set(key, promise);

    try {
      const result = await promise;
      cache.set(key, Promise.resolve(result)); // Cache resolved value
      return result;
    } catch (error) {
      cache.delete(key); // Don't cache errors
      throw error;
    }
  };
};

The Bottom Line

Memoization is like having a really good memory—it makes everything faster and smoother, but you need to be smart about what you choose to remember. Use it for expensive, pure functions with repeated inputs, and your applications will thank you with better performance and happier users.

Start small: identify one slow function in your codebase that gets called repeatedly with the same arguments. Add memoization, measure the improvement, and prepare to be impressed.

Remember: premature optimization is the root of all evil, but strategic optimization with memoization? That's just good engineering.


B
Bhargav2y ago

Good read.

1
B

Thank you!

B

Memoization in JavaScript = improved speed, reduced complexity, and scalability. Loved the practical examples! 👏

1
B

Thank you😊

1

More from this blog

T

Tech by Benson: Web Development, PHP, Laravel, React, JavaScript Tips &amp; Insights

7 posts

Benson, a seasoned programmer and tech enthusiast, shares expertise in web development, PHP, Laravel, React, and JavaScript. Join the journey of learning and growth in the ever-evolving tech world.