Because your CPU will thank you enough for doing it!
In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. Source
So the main idea here, is that you don’t have to recalculate things when you get same inputs as last time’s execution, above all when the computation is complex. This computation could be side network effects, math calculations, virtual dom calculation, text parsing, searching, arrays sorting…
Note: I’m talking about memoization of pure function which always return same output when same input(s) no matter the environment, context, time.
Please find all the source code shown in this article in my GitHub repository
We have this state object:
const state = {
filter: "active",
todos: [
{ status: "done", text: "learn memoization" },
{ status: "active", text: "say hello to me" },
{ status: "done", text: "check this demo" },
{ status: "active", text: "give me feedback" },
],
}
We want to only select todos based on the filter, without recalculating when the same inputs(todos and filter) occur again.
As you’ve read in the above definition part, memoization is used to optimize expensive function calls. So, to simulate expensive calculation, I’ve created the function below which when injected inside another one makes its execution slow.
const slowFunctionExecution = () => {
for (let index = 0; index < 50000000; index++);
}
module.exports = {
slowFunctionExecution,
}
The next function takes a function as a parameter and execute it. It also displays the passed function execution time.
const displayFunctionExecutionTime = someFunction => {
const start = Date.now()
const result = someFunction()
console.log("Execution time:", Date.now() - start, "ms")
return result
}
module.exports = {
displayFunctionExecutionTime,
}
I’ll use this function to compare none memoized and memoized function executions times later
I explain below, step by step, the next code:
const {
displayFunctionExecutionTime,
} = require("./helpers/displayFunctionExecutionTime")
const { slowFunctionExecution } = require("./helpers/slowFunctionExecution")
const noneMemoizedGetTodos = (todos, filter) => {
console.log("Getting todos...")
slowFunctionExecution()
switch (filter) {
case "done":
case "active":
return todos.filter(({ status }) => status === filter)
case "all":
default:
return todos
}
}
const state = {
filter: "active",
todos: [
{ status: "done", text: "learn memoization" },
{ status: "active", text: "say hello to me" },
{ status: "done", text: "check this demo" },
{ status: "active", text: "give me feedback" },
],
}
console.log(
`${state.filter} todos count:`,
displayFunctionExecutionTime(
() => noneMemoizedGetTodos(state.todos, state.filter).length
)
)
console.log()
console.log(
`${state.filter} todos count:`,
displayFunctionExecutionTime(
() => noneMemoizedGetTodos(state.todos, state.filter).length
)
)
First, I’ve created this pure function noneMemoizedGetTodos. It takes todos array and the filter string and return selected todos based on their status using filter array method. As said before, slowFunctionExecution is injected inside to slow the execution.
Next, we have the state containing filter defaults to 'active'
and some todos.
Finally I execute noneMemoizedGetTodos function twice with same inputs (the state object above in the code), and log the results to the console.
The next image shows the execution of noneMemoizedGetTodos.js file:
Interpretation:
As expected, this function get executed twice taking 101ms in first execution and 80ms in second one, both counting 2 active todos which is correct.
What if we can make second execution takes ≈0ms while getting same results?
In fact we can, since we already calculated the output in the first execution for the same inputs. I’ll demonstrate that in the next part about memoization.
Part 1:
In this part, I’ve implemented memoization to this specific case (getting todos based on the given filter).
For that, I’ve created 3 variables to keep track of last todos, last filter and last result. When both todos and filter don’t change, I return last kept result.
const {
displayFunctionExecutionTime,
} = require("./helpers/displayFunctionExecutionTime")
const { slowFunctionExecution } = require("./helpers/slowFunctionExecution")
let lastTodos = undefined
let lastFilter = undefined
let lastResult = undefined
const ownMemoizedGetTodos = (todos, filter) => {
if (lastTodos === todos && lastFilter === filter) {
return lastResult
} else {
// to make it simple, assign both of them even
// if at least one changed
lastTodos = todos
lastFilter = filter
console.log("Getting todos...")
slowFunctionExecution()
switch (filter) {
case "done":
case "active":
lastResult = todos.filter(({ status }) => status === filter)
return lastResult
case "all":
default:
lastResult = todos
return lastResult
}
}
}
const state = {
filter: "active",
todos: [
{ status: "done", text: "learn memoization" },
{ status: "active", text: "say hello to me" },
{ status: "done", text: "check this demo" },
{ status: "active", text: "give me feedback" },
],
}
console.log(
`${state.filter} todos count:`,
displayFunctionExecutionTime(
() => ownMemoizedGetTodos(state.todos, state.filter).length
)
)
console.log()
console.log(
`${state.filter} todos count:`,
displayFunctionExecutionTime(
() => ownMemoizedGetTodos(state.todos, state.filter).length
)
)
The next image shows the execution of ownMemoizedGetTodos.js file:
Interpretation:
As you see ownMemoizedGetTodos didn’t recalculate todos in the second call since the todos and the filter still the same as the first execution.
We’ve made our program fast by 95ms !
Note: Since ownMemoizedGetTodos uses reference equality comparison (===) to compare todos and filter with previous ones, these inputs should be immutable!
Note: You can implement caching using deep equality comparison, which doesn’t force immutability usage.
You may ask, should I implement this caching logic for all my program functions according to their specific use cases? This is lot of code to add!!
Well no, you may implement a general caching solution, which returns the cached result or recalculates new one based on if the inputs changed or not.
The good news is that you haven’t to do, since some libraries already do that for you, and this is the subject for the next part.
Part 2:
In this part, I am going to memoize getting todos using Reselect redux’s library.
This is reduxjs/reselect github repository
First, install it:
{
"name": "memoized-functions-using-reselect-demo",
"version": "1.0.0",
"author": "Anass Daoudi",
"dependencies": {
"reselect": "^4.0.0"
}
}
Reselect library provides createSelector function.
Signature:
createSelector(…inputSelectors | [inputSelectors], transformFunction)
createSelector returns a selector.
Example:
const memoizedGetTodos = createSelector(
[state => state.todos, state => state.filter],
noneMemoizedGetTodos
)
noneMemoizedGetTodos is same function as the one from solution without memoization above.
const state = { filter: someFilter, todos: someTodos }
memoizedGetTodos(state)
memoizedGetTodos(state)
To execute the memoizedGetTodos selector, you have to provide the state (some input). Then the input selectors (here we have 2, one that selects todos and the other one selects the filter) get executed. If their return values don’t change compared to their previous executions, the transform function will not execute and the cached result will be returned.
Note: by default, input selectors returned values are compared with their previous ones using reference equality comparison (===), so their inputs (input passed to memoizedGetTodos in our case which is state object) must be immutable!
Note: You can customize your selector to use deep equality comparison instead, without enforcing immutability usage.
So let’s use reselect in our main example:
const {
displayFunctionExecutionTime,
} = require("./helpers/displayFunctionExecutionTime")
const { slowFunctionExecution } = require("./helpers/slowFunctionExecution")
const { createSelector } = require("reselect")
const getTodos = (todos, filter) => {
console.log("Getting todos...")
slowFunctionExecution()
switch (filter) {
case "done":
case "active":
return todos.filter(({ status }) => status === filter)
case "all":
default:
return todos
}
}
const todosSelector = ({ todos }) => todos
const filterSelector = ({ filter }) => filter
const memoizedGetTodos = createSelector(
[todosSelector, filterSelector],
getTodos
)
const state = {
filter: "all",
todos: [
{ status: "done", text: "learn memoization" },
{ status: "active", text: "say hello to me" },
{ status: "done", text: "check this demo" },
{ status: "active", text: "give me feedback" },
],
}
console.log(
`${state.filter} todos count:`,
displayFunctionExecutionTime(() => memoizedGetTodos(state).length)
)
console.log()
console.log(
`${state.filter} todos count:`,
displayFunctionExecutionTime(() => memoizedGetTodos(state).length)
)
console.log()
// Change the state from filter:all to filter:done
const newState = { ...state, filter: "done" }
console.log(
`${newState.filter} todos count:`,
displayFunctionExecutionTime(() => memoizedGetTodos(newState).length)
)
console.log()
console.log(
`${newState.filter} todos count:`,
displayFunctionExecutionTime(() => memoizedGetTodos(newState).length)
)
Let’s execute memoizedGetTodos.js file and interpret the results:
all
.done
.As noted above, I’ve changed filter by creating new state object and not mutating old one using the following line of code:
const newState = { ...state, filter: "done" }
Update: I’ll make PART 2
of this article to go more on Memoization and simplify its usage. Things you probably don’t want to miss!
Thank you for your time.
If you have any feedback, suggestions or questions please let me know!