Deep Dive into Caching and Memoization: Maximizing JavaScript Performance

Explore caching and memoization as crucial performance optimization strategies in JavaScript and React. Understand how intelligent value storage in memory or persistent storage can enhance app efficiency.

ShareShare

Deep Dive into Caching and Memoization: Maximizing JavaScript Performance

In the realm of performance optimization, every millisecond is of paramount importance. The key to achieving faster and more efficient applications lies not in doing more but in reducing redundancy. This principle is at the core of two critical performance strategies - caching and memoization.

Caching refers to the process of storing and reusing the results of computations, network requests, or component trees that are computationally expensive, thereby avoiding the need to recompute.

Memoization, a specific form of caching, is applied to pure functions and render lifecycles. It prevents unnecessary re-computation or re-rendering by storing the results of function calls and reusing them when the same inputs occur.

In this comprehensive guide, we'll delve into:

  • The distinction between caching and memoization
  • Practical implementation patterns in JavaScript and React
  • Comparison between in-memory and persistent cache
  • Differences between server-side and client-side caching
  • Real-world application examples from React Query, Apollo, and SWR

The Importance of Caching

Implementing caching strategies can significantly enhance:

  • Load times: By retrieving data from cache, we avoid the time consumed in requesting and fetching data from the server.
  • Render speed: Caching can prevent unnecessary re-rendering of components, thereby improving the speed of the React render cycle.
  • Bandwidth efficiency: By storing data locally, we reduce the need for duplicate server requests, saving bandwidth.
  • User experience: Faster load times and smoother interactions lead to a more positive user experience.

Simultaneously, caching helps to diminish:

  • Duplicate fetches: By storing the results of a network request, we can avoid making the same request multiple times.
  • Redundant renders: Caching render results enables us to bypass redundant rendering cycles.
  • Recalculation of pure functions: By storing the results of pure function computations, we can avoid costly recalculations.

Implementing Memoization in JavaScript

function expensiveOperation(x) {
  console.log("Running...");
  return x * x;
}

const memoized = (() => {
  const cache = {};
  return (x) => {
    if (x in cache) return cache[x];
    return (cache[x] = expensiveOperation(x));
  };
})();

In the above code block, we have an expensiveOperation function that performs a calculation on input x. This operation is wrapped within a memoization function, which creates a cache object. The memoized function checks if the result of the calculation for a specific x value is already stored in the cache. If it is, the cached result is returned, bypassing the need to recompute. If not, the calculation is performed, and the result is stored in the cache for future use. Thus, when you call memoized(5) multiple times, the expensive operation only runs once, and subsequent calls return the cached result.


Memoization in React: useMemo(), useCallback(), and React.memo()

React provides several hooks and utilities to help us implement memoization:

useMemo()

const computedValue = useMemo(() => heavyCalculation(input), [input]);

The useMemo hook allows us to cache computational heavy values. It takes a function and a dependency array as arguments. The function runs only when the dependencies change, and the result is stored and reused in subsequent renders unless the dependencies change.

useCallback()

const handleClick = useCallback(() => doSomething(id), [id]);

useCallback is similar to useMemo, but it returns a memoized version of the callback function that only changes if one of the dependencies changes. This is particularly useful to prevent unnecessary re-renders of child components when passing functions as props.

React.memo()

const ListItem = React.memo(({ item }) => {
  return <div>{item.label}</div>;
});

React.memo() is a higher-order component that ensures a component only re-renders when its props change. It can be combined with useMemo and useCallback to optimize React applications deeply.


Caching Network Data: SWR and React Query

Two popular libraries that provide hooks for data fetching and caching in React are SWR and React Query.

SWR

const { data } = useSWR("/api/user", fetcher);

SWR (stale-while-revalidate) caches network requests results in memory. It supports time-to-live (TTL), enabling automatic data refresh after a specified time period, and stale-while-revalidate, which returns stale data while fetching new data.

React Query

useQuery(["posts"], fetchPosts, {
  staleTime: 60 * 1000
});

React Query also provides in-memory caching and supports cache-first strategies. It's pagination-aware, ensuring efficient data loading for large datasets.


Persistent Caches: IndexedDB, localStorage, sessionStorage

For long-term data storage and offline support, persistent caches like IndexedDB, localStorage, or sessionStorage can be utilized.

localStorage.setItem("settings", JSON.stringify(data));

In the above example, we're using the localStorage API to store a "settings" object. However, it's crucial to manage persistent caches carefully to avoid stale data and maintain performance. Always keep the stored data small and scoped, and combine it with TTL for automatic data invalidation.


Real-World Patterns: GitHub, VSCode Web, Notion

Several high-performing web applications employ caching and memoization strategies:

GitHub

GitHub caches issue lists and pull request data across tabs to avoid redundant network requests within the same session.

VSCode Web

VSCode Web uses memoization for settings, file structure, and recent activity data. It uses IndexedDB for persistent storage, enabling data retention across page reloads.

Notion

Notion effectively caches block trees for different workspace views. It also memoizes block rendering to prevent slow document tree rebuilding.


Anti-Patterns to Avoid

While caching and memoization can greatly enhance performance, improper usage can lead to issues:

  • Memoizing non-pure functions: Memoization should only be used with pure functions. Using it with functions that have side effects can lead to incorrect results.
  • Over-memoizing: Overuse of useMemo and useCallback can lead to memory bloat and reduced performance.
  • Caching dynamic data without invalidation: Always ensure to invalidate and update cached data that changes frequently.
  • Using useMemo to fix prop drilling: Prop drilling issues should be solved by restructuring the component tree or implementing context, not by memoizing props.

Conclusion: Thoughtful Optimization for Sustainable Performance

Fast and efficient applications are not built merely by optimizing the initial load and render but by ensuring sustained performance.

By intelligently implementing memoization and caching, developers can avoid unnecessary computations and network requests. As a result, applications maintain high performance by reusing previously computed results, saving both time and computational resources.

Remember, the goal of smart engineering is to avoid doing more than necessary. Think once, remember often, render fast.

Subscribe for Deep Dives

Get exclusive in-depth technical articles and insights directly to your inbox.

about

Ehsan Hosseini

Ehsan Hosseini

me [at] ehosseini [dot] info

Staff Software Engineer and Tech Lead with a track record of leading high-performing engineering teams and delivering scalable, end-to-end technology solutions. With experience across web, mobile, and backend systems, I help companies make smart architectural decisions, scale efficiently, and align technical strategy with business goals.

© 2025 Ehsan Hosseini. All rights reserved.