"There are only two hard problems in Computer Science: cache invalidation and naming things."
- Phil Karlton
Caching API calls is a common way to improve web app performance since caching reduces unnecessary and redundant API calls. Some common scenarios where unnecessary API calls might be happening are,
If any of the repeated calls provide the same data every time, we could avoid them by caching and also save the round trips. It will reduce the load on the server and improve the performance of your web application at the same time.
The browser's HTTP cache should be the first choice for caching API calls because it is effective, supported in all browsers, and easy to implement. Caching of an API call in the browser's HTTP cache is controlled by its request headers and response headers.
Here is a good article on configuring HTTP cache on the client and server-side - https://web.dev/http-cache/
For a deeper understanding of HTTP caching, please check out MDN's HTTP caching article - https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
In conclusion, the browser’s HTTP caching works in a combined effort of both client and server-side configurations. So we need to have control over both web application and web server for configuring request and response headers to make HTTP caching work as per our requirement. But sometimes we might not have control over server-side configurations or the server might not be sending all the required cache headers in the response.
In this article, let’s discuss one way to cache API calls in a web app using JavaScript if HTTP cache is not working for you.
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. This is the Wikipedia definition of memoization and we are going to use this technique to cache our API calls.
In the case of API calls, the result will be a promise and promises are javascript objects, so we can store it in a map data structure (calling it `cache`) with a unique key for each request. If the same API is called with the same key again, we can return the promise from the cache without fetching it again from the server.
To make our solution work, we will implement a higher-order function called memoizePromiseFn
that takes a function that returns a promise
as input and return memoized version of the function. We can use the output from memoizePromiseFn
to make API calls and the results will be cached automatically with the key being the arguments array
to the function. So if the function is called with the same arguments again, the result will be served from the cache. Here is our function that memoises a function that returns a promise,
const memoizePromiseFn = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
cache.set(key, fn(...args).catch((error) => {
// Delete cache entry if API call fails
cache.delete(key);
return Promise.reject(error);
}));
return cache.get(key);
};
};
The input for this function will be our service function that makes the API call to server and returns the result. For example,
function fetchTodo(id) {
return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then((response) => response.json())
.then((json) => json);
}
We can cache calls to the above function with id
parameter as key using our promiseMemoize
function. So if the fetchTodo
function is called with the same id
second time, response will be served from the cache without a roundtrip to the server. In case of an API call failure, the cached value will be cleared for the particular key.
async function getTodos() {
// Here the higer order function is called
let cachedFetchTodos = memoizePromiseFn(fetchTodo);
let response1 = await cachedFetchTodos(1); // Call to server with id 1
let response2 = await cachedFetchTodos(2); // Call to server with id 2
let response3 = await cachedFetchTodos(1); // id is 1, will be served from cache
let response4 = await cachedFetchTodos(3); // Call to server with id 3
let response5 = await cachedFetchTodos(2); // id is 2, will be served from cache
// Total number of calls - 3
}
getTodos();
You can view demo in
Github repo - https://github.com/aldrinpvincent/memoize-promise-fn
If you are using react, place call to
memoizePromiseFn
ie,
let cachedFetchTodos = memoizePromiseFn(fetchTodo);
outside the react component. Otherwise, on every render, a different instance of memoized function will get created and the caching will not work as expected.
For memoization to work properly, the input function should be a pure function, because if the output of the function is different for the same input at different points in time, caching won’t work and you will run into stale data issues since the first result will be served every time.
Also, this is not a proper caching mechanism since the cache will get cleared on page reload. This mechanism may work for a single page application where routing happens on the client side without a full page reload and is applicable only for API calls whose response won’t change frequently.