Part 92: Caching and Cookies in LWC
Every time a user loads a Lightning page, something happens behind the scenes that most developers never think about. The browser makes network requests. Apex runs on the server. SOQL queries hit the database. Components render. And if you have not thought about caching, every single one of those steps happens fresh, every single time. That is slow, expensive, and completely unnecessary for data that does not change often.
If you have been following this series, you already know how to build LWC components, call Apex, handle events, and manage state. But we have not yet talked about one of the most practical performance tools available to you — caching. Back in Part 49, we covered Platform Cache from the Apex side, looking at how to store and retrieve data in org cache and session cache. This post picks up that thread and brings it into the LWC world, while also covering two browser-native storage mechanisms that Salesforce developers often overlook: localStorage, sessionStorage, and cookies.
This post covers:
- What is Caching and Why Bother With It — Understanding the problem caching solves and when it matters.
- How to Use the Browser Cache in Your LWC — Working with localStorage and sessionStorage for client-side data persistence.
- How to Leverage Cookies in Your LWC — Reading and writing cookies from Lightning Web Components.
- How to Use the Platform Cache in Your LWC — Calling Apex-backed platform cache from your components.
- Section Notes — Key takeaways, limitations, and best practices.
Let’s get into it.
What is Caching and Why Bother With It
Caching is the practice of storing a copy of data in a location that is faster or cheaper to access than the original source. Instead of querying the database every time a user opens a page, you store the result somewhere closer — in the browser, in memory on the server, or in a dedicated cache layer — and serve that stored copy on subsequent requests.
The reason this matters in Salesforce is straightforward. Every Apex call counts against your governor limits. Every SOQL query has a ceiling. Every network round trip adds latency that users feel. If your component displays a list of product categories that changes once a month, there is no reason to query the database every time the page loads. Store it once, serve it from cache, and refresh it on a schedule or when the data actually changes.
There are three layers of caching available to you as an LWC developer:
Browser-side storage includes localStorage and sessionStorage. These are built into every modern browser and let you store key-value pairs directly on the client machine. localStorage persists across browser sessions. sessionStorage lasts only until the tab is closed. Both are synchronous, fast, and require zero server interaction.
Cookies are another browser-side mechanism, but they behave differently. Cookies are sent with every HTTP request to the server, they have size limits, and they support expiration dates. In Salesforce, cookies are useful for tracking preferences, managing simple state across page navigations, and integrating with external systems.
Platform Cache is the server-side option. It lives on the Salesforce infrastructure, supports both org-wide and session-scoped storage, and is accessible from Apex. Your LWC calls an Apex method, and that method reads from or writes to the platform cache instead of querying the database.
Each layer has trade-offs. Browser storage is fast but limited in size and lives only on one device. Cookies are small and travel with every request. Platform cache is shared across users (in org cache) but still counts as a server call from the LWC perspective. The best caching strategy often uses more than one layer.
How to Use the Browser Cache in Your LWC
The browser gives you two storage APIs that work identically in terms of their interface: localStorage and sessionStorage. Both store string key-value pairs. The difference is lifespan. localStorage sticks around until you explicitly remove it or the user clears their browser data. sessionStorage disappears when the browser tab closes.
Writing to localStorage
Here is a simple example. Suppose your component loads a list of picklist values from Apex, and those values rarely change. Instead of calling Apex every time, you can cache the result in localStorage.
import { LightningElement, wire } from 'lwc';
import getPicklistValues from '@salesforce/apex/PicklistController.getPicklistValues';
const CACHE_KEY = 'product_categories';
const CACHE_DURATION = 1000 * 60 * 60; // 1 hour in milliseconds
export default class CategoryPicker extends LightningElement {
categories = [];
connectedCallback() {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const parsed = JSON.parse(cached);
const age = Date.now() - parsed.timestamp;
if (age < CACHE_DURATION) {
this.categories = parsed.data;
return;
}
}
this.loadFromServer();
}
loadFromServer() {
getPicklistValues()
.then(result => {
this.categories = result;
const cacheEntry = {
data: result,
timestamp: Date.now()
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheEntry));
})
.catch(error => {
console.error('Failed to load categories', error);
});
}
}
The pattern here is important. You check the cache first. If the data exists and is not stale, you use it immediately — no server call, no latency. If the cache is empty or expired, you call Apex, store the fresh result, and move on. The timestamp lets you control how long the cached data is considered valid.
Using sessionStorage for Temporary Data
sessionStorage works the same way but is scoped to the current browser tab. This is useful for data that should not persist across sessions — form progress, temporary filters, or intermediate calculation results.
// Save form progress so the user does not lose work on page refresh
saveFormProgress() {
const formData = {
name: this.nameValue,
email: this.emailValue,
step: this.currentStep
};
sessionStorage.setItem('intake_form_progress', JSON.stringify(formData));
}
// Restore form progress when the component loads
restoreFormProgress() {
const saved = sessionStorage.getItem('intake_form_progress');
if (saved) {
const formData = JSON.parse(saved);
this.nameValue = formData.name;
this.emailValue = formData.email;
this.currentStep = formData.step;
}
}
Clearing Cached Data
You can remove individual entries or clear everything:
// Remove a specific key
localStorage.removeItem('product_categories');
// Clear all localStorage for this origin
localStorage.clear();
// Same methods work for sessionStorage
sessionStorage.removeItem('intake_form_progress');
sessionStorage.clear();
Limitations to Know About
There are a few things to keep in mind with browser storage in the Salesforce context.
First, both localStorage and sessionStorage are scoped to the origin — the protocol, domain, and port. In Salesforce, your Lightning pages run on a domain like yourorg.lightning.force.com. All your LWC components on that domain share the same storage space. That means key collisions are possible if two components use the same key name. Use descriptive, namespaced keys like myApp_productCategories to avoid this.
Second, storage limits vary by browser but are typically around 5-10 MB per origin. That is plenty for configuration data and small datasets, but not suitable for storing large record collections.
Third, localStorage and sessionStorage only store strings. You have to serialize objects with JSON.stringify and deserialize with JSON.parse. If you try to store an object directly, you will get [object Object] as the stored value, which is useless.
Fourth, and this is specific to Salesforce — if your org uses multiple domains or if users switch between Lightning Experience and classic, the storage context changes. Data cached on one domain is not visible on another.
How to Leverage Cookies in Your LWC
Cookies are older than localStorage and work differently. A cookie is a small piece of data that the browser stores and automatically includes in HTTP requests to the server. Cookies have built-in support for expiration dates, path scoping, and security flags.
In LWC, you interact with cookies through the document.cookie API. It is not the most elegant API — reading cookies requires parsing a semicolon-delimited string, and writing cookies requires constructing a formatted string — but it works.
Writing a Cookie
setCookie(name, value, days) {
let expires = '';
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = '; expires=' + date.toUTCString();
}
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/; Secure; SameSite=Strict';
}
The Secure flag ensures the cookie is only sent over HTTPS. The SameSite=Strict flag prevents the cookie from being sent in cross-site requests, which is a basic security measure. Always include both in production code.
Reading a Cookie
getCookie(name) {
const nameEQ = name + '=';
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i].trim();
if (cookie.indexOf(nameEQ) === 0) {
return decodeURIComponent(cookie.substring(nameEQ.length));
}
}
return null;
}
Deleting a Cookie
To delete a cookie, you set its expiration date to the past:
deleteCookie(name) {
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
}
A Practical Example — User Preferences
Here is a component that remembers whether the user prefers a compact or expanded view, using a cookie that lasts 30 days:
import { LightningElement } from 'lwc';
export default class UserPreferences extends LightningElement {
isCompactView = false;
connectedCallback() {
const preference = this.getCookie('view_preference');
if (preference === 'compact') {
this.isCompactView = true;
}
}
handleToggleView() {
this.isCompactView = !this.isCompactView;
const value = this.isCompactView ? 'compact' : 'expanded';
this.setCookie('view_preference', value, 30);
}
setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = '; expires=' + date.toUTCString();
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/; Secure; SameSite=Strict';
}
getCookie(name) {
const nameEQ = name + '=';
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i].trim();
if (cookie.indexOf(nameEQ) === 0) {
return decodeURIComponent(cookie.substring(nameEQ.length));
}
}
return null;
}
}
When to Use Cookies vs localStorage
Use cookies when you need the data to be available on the server side — cookies travel with HTTP requests automatically. Use cookies when you need built-in expiration without writing your own timestamp logic. Use cookies for small values like preferences, feature flags, or session identifiers.
Use localStorage when you need more storage space, when the data is only needed on the client, or when you do not want the overhead of sending extra data with every HTTP request. For most LWC use cases, localStorage is the better choice unless you have a specific reason to use cookies.
One important note: cookie size is limited to about 4 KB per cookie, and browsers typically allow around 20-50 cookies per domain. That is significantly less than the 5-10 MB available in localStorage.
How to Use the Platform Cache in Your LWC
Platform Cache is the server-side caching solution on Salesforce. Unlike browser storage and cookies, platform cache lives on Salesforce servers and is accessible from Apex. There are two partitions: Org Cache, which is shared across all users in the org, and Session Cache, which is scoped to an individual user session.
We covered the Apex side of platform cache in detail back in Part 49. Here, we are focused on how your LWC components can take advantage of it.
The pattern is straightforward. Your LWC calls an Apex method. That Apex method checks the platform cache before hitting the database. If the cache has the data, it returns immediately. If not, it queries the database, stores the result in the cache, and then returns it. From the LWC’s perspective, it is just a normal Apex call — the caching logic is entirely server-side.
The Apex Controller
public with sharing class CachedDataController {
@AuraEnabled(cacheable=true)
public static List<Product2> getActiveProducts() {
// Check org cache first
List<Product2> products = (List<Product2>) Cache.Org.get('local.ProductCache.activeProducts');
if (products != null) {
return products;
}
// Cache miss — query the database
products = [
SELECT Id, Name, ProductCode, Family, Description
FROM Product2
WHERE IsActive = true
ORDER BY Name
LIMIT 200
];
// Store in org cache with a 1-hour TTL
Cache.Org.put('local.ProductCache.activeProducts', products, 3600);
return products;
}
@AuraEnabled
public static void clearProductCache() {
Cache.Org.remove('local.ProductCache.activeProducts');
}
}
Notice the @AuraEnabled(cacheable=true) annotation on the getter. This tells the LWC framework that the method’s return value can also be cached on the client side by the Lightning Data Service. So you actually get two layers of caching here — the LWC framework caches the Apex response on the client, and the Apex method itself caches the query result on the server. When both are warm, the component renders almost instantly.
The LWC Component
import { LightningElement, wire } from 'lwc';
import getActiveProducts from '@salesforce/apex/CachedDataController.getActiveProducts';
import clearProductCache from '@salesforce/apex/CachedDataController.clearProductCache';
import { refreshApex } from '@salesforce/apex';
export default class ProductList extends LightningElement {
products = [];
error;
wiredProductsResult;
@wire(getActiveProducts)
wiredProducts(result) {
this.wiredProductsResult = result;
if (result.data) {
this.products = result.data;
this.error = undefined;
} else if (result.error) {
this.error = result.error;
this.products = [];
}
}
handleRefresh() {
clearProductCache()
.then(() => {
return refreshApex(this.wiredProductsResult);
})
.then(() => {
console.log('Cache cleared and data refreshed');
})
.catch(error => {
console.error('Failed to refresh', error);
});
}
}
The handleRefresh method shows how to invalidate the cache. It calls the Apex method that removes the cached entry from platform cache, then uses refreshApex to force the wired method to re-execute. This gives the user a way to manually refresh the data when they know something has changed.
Combining Client and Server Caching
For the best performance, you can layer browser storage on top of platform cache. The first time the component loads, it calls Apex (which checks platform cache, then the database). The LWC stores the result in localStorage with a timestamp. On subsequent page loads, the component checks localStorage first. If the data is fresh enough, it skips the Apex call entirely.
import { LightningElement } from 'lwc';
import getActiveProducts from '@salesforce/apex/CachedDataController.getActiveProducts';
const LOCAL_CACHE_KEY = 'cached_active_products';
const LOCAL_CACHE_TTL = 1000 * 60 * 30; // 30 minutes
export default class ProductListOptimized extends LightningElement {
products = [];
connectedCallback() {
const cached = localStorage.getItem(LOCAL_CACHE_KEY);
if (cached) {
const parsed = JSON.parse(cached);
if (Date.now() - parsed.timestamp < LOCAL_CACHE_TTL) {
this.products = parsed.data;
return;
}
}
this.fetchProducts();
}
fetchProducts() {
getActiveProducts()
.then(result => {
this.products = result;
localStorage.setItem(LOCAL_CACHE_KEY, JSON.stringify({
data: result,
timestamp: Date.now()
}));
})
.catch(error => {
console.error('Error loading products', error);
});
}
}
This three-layer approach — localStorage, then LWC client cache, then platform cache, then the database — means the database only gets hit when the data is genuinely stale across all layers. For data that changes infrequently, this can eliminate the vast majority of database queries.
Platform Cache Setup Requirements
Before you can use platform cache in Apex, you need to set up a cache partition in Salesforce Setup. Go to Setup, search for Platform Cache, and create a partition. Assign capacity to the org cache and session cache sections. The default allocation depends on your Salesforce edition — Enterprise Edition gets 10 MB, Unlimited gets 30 MB, and Performance gets 30 MB. You can purchase additional capacity if needed.
The partition name matters because it appears in your cache keys. In the examples above, local.ProductCache.activeProducts breaks down as: local means the default namespace (for unmanaged code), ProductCache is the partition name, and activeProducts is the key.
Section Notes
Here are the key points to remember about caching and cookies in LWC:
localStorage is your best option for caching data that should persist across browser sessions and page reloads. Use it for reference data, user preferences, and any dataset that changes infrequently. Always include a timestamp so you can implement time-based expiration. Use namespaced keys to avoid collisions with other components.
sessionStorage is ideal for temporary data that should not outlive the current browser tab. Form progress, intermediate state, and temporary filters are good candidates. It has the same API as localStorage, so switching between them is trivial.
Cookies are best when you need the data to travel with HTTP requests or when you want built-in expiration handling. They are limited in size (about 4 KB per cookie) and should be reserved for small values. Always set the Secure and SameSite flags in production.
Platform Cache is the server-side solution. Use org cache for data shared across all users (picklist values, configuration, reference data) and session cache for user-specific data. The @AuraEnabled(cacheable=true) annotation adds an additional client-side caching layer managed by the LWC framework.
Layered caching delivers the best results. Check localStorage first to avoid a server call entirely. If that misses, let the wired Apex method check platform cache. Only hit the database as a last resort. This approach dramatically reduces load times and governor limit consumption.
Cache invalidation is the hard part. Whenever the underlying data changes, you need a strategy for clearing stale cache entries. For browser storage, time-based expiration is the simplest approach. For platform cache, you can call a dedicated Apex method to remove specific keys. For the LWC client cache, refreshApex forces a fresh server call.
Security considerations matter. Never store sensitive data like tokens, passwords, or PII in localStorage or cookies. Browser storage is accessible to any JavaScript running on the same origin, and cookies can be intercepted if not properly secured. Platform cache is the safest option for sensitive data since it stays on the server.
That covers the major caching strategies available to you in LWC. Whether you are optimizing a data-heavy dashboard, preserving user preferences across sessions, or reducing Apex callouts on a high-traffic page, these tools give you fine-grained control over where your data lives and how often you need to fetch it fresh.