What Is Memoization and Why Does It Matter?
Memoization is a programming technique that accelerates performance by caching the return values of expensive function calls. A memoized function will immediately output a pre-computed value if its given inputs that its seen before. Memoization is a specific form of caching that lends itself to scenarios where a costly function is executed repeatedly, sometimes with the same arguments. Provided that the function is pure so that it always produces the same value from a particular set of inputs, memoizing it can increase efficiency and reduce wasted CPU cycles. Youll most often encounter memoization in functional programming languages. The technique has broad utility, though. Its an abstract concept that you can incorporate into any recursive code. Were using JavaScript for this article, but you could rewrite the examples into your working language. A Simple Example  Heres a simple function that generates the factorial of a given integer:  const factorial = n => { if ( n === 0 ) return 1 ; else return ( factorial ( n - 1 ) * n ) ; } ;  The factorial calculation is recursive, as factorial() calls itself within the else condition. The calculation is also pure, as any given value of n will always return the same value. factorial(5) is 120 , irrespective of the state of the program. Advertisement    Because of the recursive nature of the function, computing the factorial of several large numbers incurs wasted operations:  const x = factorial ( 100 ) ; const y = factorial ( 50 ) ;  All the calculations needed to compute y were already performed as part of the computation of x . If factorial() were to cache its inputs and their corresponding outputs, the calculation of y could be significantly sped up. Memoizing the Factorial Function  Heres a basic approach that memoizes the factorial() function:  const cache = { } ; const factorial = n => { if ( cache [ n ] ) { return cache [ n ] ; } let value ; if ( n === 0 ) value = 1 ; else value = ( factorial ( n - 1 ) * n ) ; cache [ n ] = value ; return value ; } ;  Now, theres a cache object that factorial() uses to record its output values. Each time the function is called, it first checks whether its previously seen the input n . If it has, it can short circuit immediately and return the cached value. Otherwise, the recursive calculation proceeds as normal, but subsequent runs using the same number will be sped up. Now, computing factorial(50) after factorial(100) will be vastly more efficient. The factorial of 50 would be calculated as part of the factorial of 100 , so the function could return the value almost instantaneously. A More General Solution  While the above code works, its specific to the factorial() function. If you were using other similar functions, youd need to manually add the caching code to each one. A more general solution would let you wrap any function with a higher-order function that provides the memoizing capability:  const memoize = fn => { const cache = { } ; return ( ... args ) => { const argsString = JSON. stringify ( args ) ; if ( ! cache [ argsString ] ) { cache [ argsString ] = fn ( ... args ) ; } return cache [ argsString ] ; } } ;  Now, you can adjust the original factorial() function:  const factorial = memoize ( n => { if ( n === 0 ) return 1 ; else return ( factorial ( n - 1 ) * n ) ; } ) ;  Advertisement    By wrapping factorial() with memoize() , it gains automatic memoization capabilities. The wrapper function returns a new function that intercepts all calls to factorial() , stringifies their arguments, and checks whether theyve been seen before. If they have, the previous return value is reused without calling the real function at all. When new arguments are seen, the function gets invoked, and its output is added to the cache. The wrapper uses the JavaScript rest parameters syntax to accept a variadic number of arguments. This means that it will work with any JavaScript function. This approach is using JSON.stringify() to create the string representation of the arguments, so care should be taken if youre calling a function with complex objects, which cant be fully represented as JSON. Memoization can deliver significant performance improvements, particularly to mathematically heavy operations. It isnt a technique to use everywhere, though. Not all functions should be memoized, as you could end up harming performance in some cases. By wrapping a function with memoize() , youre forcing a comparison of the input arguments each time that function is called. This in itself will consume some CPU time. The example wrapper above runs JSON.stringify() every time that the function is called, adding a new overhead. Memoization is appropriate for functions where theres a high chance that the same input values will be seen regularly. If youll rarely call a function with the same arguments, cache hits will be infrequent, and you might lose performance to argument serialization and comparison. Maintaining the cache also increases memory use, as all previous inputs and outputs need to be retained. You should, therefore, fully assess each functions role in your program before deciding to memoize. Compare performance with and without memoization. Sometimes, it can prove more beneficial to rely on the browsers own optimizations. Advertisement    Finally, bear in mind that some functions cant be memoized. The technique only works with pure functionsif your function reaches out to global variables or some other application state, it should not be memoized! const functionUsingGlobalState = n => { if ( n === window. scrollY ) return true ; else return functionUsingGlobalState ( n - 1 ) ; }  Memoizing the function above could yield unexpected results after the first run. Values of n will be cached using the original window.scrollY version. The memoization wrapper will return the cached output, even if window.scrollY has changed. Summary  Memoization is a form of caching that accelerates the performance of repetitive recursive operations. Its a functional programming technique that can be implemented as a generic wrapper for any pure function. Youll most often encounter memoization in frequently called functions that perform computationally heavy operations. It helps avoid waste by removing the need to recalculate values that have already been produced as part of a previous call. The benefits of memoization will be less apparent in functions that are simple to begin with or infrequently called. In some cases, improper use of memoization can actually harm performance, so dont blindly memoize every function in your application.