Have you ever had numerous repetitive callouts in LWC and couldn't rely on the platform's caching mechanism?
When working with Lightning Web Components, I recently had a use case where I needed to fetch data based on the current user's selection.
Since users could change the selection and further interact with the UI, I needed a caching solution to improve performance and eliminate unnecessary callouts.
Salesforce has the Lightning Data Service, which has an in-built caching mechanism.
It's a best practice to utilize that to fetch the data.
Unfortunately, in my case, I was bound by the custom Apex implementation, and the data structure was very complex.
Plenty of libraries have memoization functions to cache the results of the functions: e.g., lodash memoize.
However, I couldn't rely on the third-party libraries because the development context is for the Managed Package, and I didn't want to introduce new dependencies.
Eventually, I devised a memoization utility function for promises to use inside my Lightning Web Components.
In this post, I would like to walk you through how to create such a utility function and offer a few tips for improvements.
What is memoization?
Memoization is a type of caching, an optimization technique used to speed up programs by storing the results of expensive operations and returning the cached result when the same input occurs again.
One of the most famous examples is the Nth Fibonacci recursive algorithm with a memoization technique that helps reduce the time complexity from O(2^n) to O(n) by making sure we calculate the result for each input just once.
Base case
Let's see how we can create a base memo function in JavaScript.
export const memoize = (fn) => {
const cache = new Map();
const memoFn = (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
cache.set(
key,
fn(...args).catch((error) => {
cache.delete(key);
return Promise.reject(error);
})
);
return cache.get(key);
};
return memoFn;
};
It accepts the function you want to memoize and returns its memoized version.
The main idea is to create a closure that will handle the cache, store results, and return them if present.
The closure is created every time the function is created, and it allows it to access the outer scope.
You can read more on the topic here.
Essentially, we create a cache variable that our function can access during the execution.
The key is a stringified list of arguments to remember the arguments from previous executions.
If the cache map has the result, it will return it without calling a function.
Otherwise, it will call the function and store its result in a map.
If there are any errors, it will reject and delete the cache entry.
Having such a function, you can easily implement it to memoize any of the component's methods.
import { LightningElement } from "lwc";
import { memoize } from "c/memoize";
// Import apex method
import sampleApexMethod from "@salesforce/apex/SampleController.sampleApexMethod";
export default class SampleComponent extends LightningElement {
constructor() {
this.sampleComponentMethod = memoize(this.sampleComponentMethod.bind(this));
}
// A sample method that calls apex
async sampleComponentMethod() {
try {
// ...
await sampleApexMethod();
} catch (e) {
// ...
}
}
}
Maximum cache size
One of the improvement considerations is the maximum size of the cache. Do you want to limit it?
We can achieve that by creating a new configuration argument.
For instance, we can have a default value of 100 and allow a modification.
The main idea is to check if the current cache size is larger than the limit.
If it is, then we delete the first key so that we can store one more.
export const memoize = (fn, { maxSize = 100 } = {}) => {
const cache = new Map();
const memoFn = (...args) => {
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
cache.set(
key,
fn(...args).catch((error) => {
cache.delete(key);
return Promise.reject(error);
})
);
return cache.get(key);
};
return memoFn;
};
Custom key resolver
Another improvement is having a custom resolver for the cache key.
In my case, the payload for the callout is very complex and extensive.
There are a few caveats with this.
First of all, a stringified key is unnecessary and huge.
Then, the order of the properties within JSON is not guaranteed, so even if the payload remains the same but the order of the properties changes, it will result in a callout.
The third reason is that we don't care about the whole object being changed. Sometimes, we care only about specific props.
In my example, I only worried about the selected contact and account IDs.
As a solution, we can create an optional resolver function that takes the payload as input and transforms it into a key for the cache map.
Here is an example with the custom resolver added.
export const memoize = (fn, { maxSize = 100, resolver } = {}) => {
const cache = new Map();
const memoFn = (...args) => {
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
const key = resolver ? resolver(...args) : JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
cache.set(
key,
fn(...args).catch((error) => {
cache.delete(key);
return Promise.reject(error);
})
);
return cache.get(key);
};
return memoFn;
};
import { LightningElement } from "lwc";
import { memoize } from "c/memoize";
// Import apex method
import sampleApexMethod from "@salesforce/apex/SampleController.sampleApexMethod";
export default class SampleComponent extends LightningElement {
constructor() {
this.sampleComponentMethod = memoize(
this.sampleComponentMethod.bind(this),
{
resolver: (...args) => {
// process the args and return a string key
},
}
);
}
// A sample method that calls apex
async sampleComponentMethod() {
try {
// ...
await sampleApexMethod();
} catch (e) {
// ...
}
}
}
Clear cache
One more consideration is related to cache clearance.
Sometimes, you have a situation when you want to clear the cache manually.
Fortunately, it is very easy to achieve.
In JavaScript, every function is essentially an object, and you can attach methods to it.
Here is an example of the clearCache method we can expose with the memoized function.
export const memoize = (fn, { maxSize = 100, resolver } = {}) => {
const cache = new Map();
const memoFn = (...args) => {
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
const key = resolver ? resolver(...args) : JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
cache.set(
key,
fn(...args).catch((error) => {
cache.delete(key);
return Promise.reject(error);
})
);
return cache.get(key);
};
memoFn.clearCache = () => {
cache.clear();
};
return memoFn;
};
Whenever you need to clear the cache, you can call it like this:
// given that you have the memoized function
this.sampleComponentMethod.clearCache();
Tips and improvement ideas
Another potential improvement is using the WeakMap vs. Map to handle the cache.
The difference between Map and WeakMap is that with Map, the array of keys keeps references to key objects, preventing them from being garbage collected.
In WeakMap, the references are weak, so if there is no reference to an object anymore, it can be automatically garbage collected.
Note that WeakMap cannot use primitive values as keys and has to use objects. Plus, the keys aren't enumerable, so you cannot get a list of keys or a size prop.
The main benefit of using the WeakMap is that you effectively have an infinite cache size because results will be kept in memory as long as references to the arguments still exist and then cleared out as the arguments are garbage-collected.
However, there will be no way to alter the argument equality.
You can check more on the topics here:
- https://dev.to/thekashey/weak-memoization-in-javascript-4po6
- https://reselect.js.org/api/weakmapmemoize/
Conclusion
In conclusion, implementing a custom memoization utility function for promises in Lightning Web Components can significantly enhance performance by reducing unnecessary callouts.
While leveraging built-in features like Lightning Data Service is ideal, sometimes constraints push developers toward custom solutions.
By tailoring the memoization functionality to your application's specific requirements, you can optimize performance while maintaining cleaner code.
Additionally, exploring advanced caching techniques such as WeakMap for memory management could also offer further enhancements.

Nikita Verkhoshintcev
Senior Salesforce Consultant
I'm a senior Salesforce consultant based in Helsinki, Finland, specializing in Experience Cloud, Product Development (PDO), and custom integrations. I've worked as an independent consultant building custom Salesforce communities and applications since 2016. Usually, customers reach out to me because of my expertise, flexibility, and ability to work closely with the team. I'm always open to collaboration, so feel free to reach out!