We interact with hundreds of applications and web apps every day—opening news sites and instantly seeing headlines from around the world, browsing social networks and scrolling through endless feeds. We rarely stop to think about the incredible speed at which information travels. A single request reaches a server and returns a response in seconds or less. This near-instantaneous experience depends on many factors, with caching being one of the most crucial.
This article explores the three main client-side storage technologies: localStorage, sessionStorage, and IndexedDB.
In the modern web development landscape, efficient client-side data management has become a fundamental component for creating performant and responsive applications.
Web applications of all kinds need well-defined caching strategies to reduce backend calls and improve user experience.
Frontend caching: when, how, and why
Frontend application caching represents a set of techniques for temporarily storing data on the user’s device. In web applications, this strategy is particularly advantageous for several reasons.
benefits
- Reduced HTTP calls to the backend
- Smoother user experience with immediate response times
- Offline or partially offline functionality
- Decreased server load and resource savings
When to use caching
- When data changes rarely (example: general configurations, dictionaries, enumerables)
- Frequently requested data with low update frequency (example: list of recent scheduled activities, recent todos, etc.)
- When we need to temporarily store application state (example: filters or wizard steps)
- When we want to show users immediate data while loading more current data in the background
Important Considerations
- Cache duration management: Each data type has a different lifespan, and we need to handle refresh mechanisms based on what we’re storing. For example, if we cache enumerable data, we’ll need to manage data refresh with an expiration mechanism.
- Additional logic required: We need to write logic that reads from cache instead of requesting from the backend. In Angular applications, this logic can be implemented in services.
- Browser-specific storage: Unlike backend caches that are shared across all incoming client requests, frontend cache is unique per browser. When users clear their browser “cache,” all our stored data will be lost and must be requested from the backend again. It’s important to test every scenario during development, including cases where the cache is empty.
LocalStorage
LocalStorage offers persistent storage that survives even after closing the browser, making it ideal for data that needs to be maintained long-term, such as tokens or configuration information.
Regardless of framework, the web storage APIs provide several methods to insert, remove, and update data:
localStorage.setItem('my-key-item', payload);
localStorage.getItem('my-key-item');
localStorage.removeItem('my-key-item');
localStorage.clear();Here’s an example in the context of an Angular application:
// Angular service for managing localStorage
@Injectable({
providedIn: 'root'
})
export class LocalStorageService {
saveUserPreferences(preferences: UserPreferences): void {
localStorage.setItem('userPreferences', JSON.stringify(preferences));
}
getUserPreferences(): UserPreferences | null {
const data = localStorage.getItem('userPreferences');
return data ? JSON.parse(data) : null;
}
clearUserPreferences(): void {
localStorage.removeItem('userPreferences');
}
}Limitations
- Limited space (typically 5MB)
- Only stores text strings (requires serialization/deserialization via JSON.stringify(payload))
- Synchronous data access APIs
- No support for complex queries
- Best for simple objects: The more complex the stored data, the longer serialization/deserialization takes, degrading user experience (because the process is synchronous)
SessionStorage
SessionStorage is similar to localStorage but only maintains data for the browser session duration. Example use cases include temporary saving of search filter data or temporary state of an incomplete process.
The APIs are the same as before, except we use sessionStorage instead of localStorage key:
sessionStorage.setItem('my-key-item', payload);
sessionStorage.getItem('my-key-item');
sessionStorage.removeItem('my-key-item');
sessionStorage.clear();Angular implementation:
@Injectable({
providedIn: 'root'
})
export class WizardStateService {
saveFilters(pageName: string, filtersData: FormFilters ): void {
sessionStorage.setItem(filters_${pageName}, JSON.stringify(filtersData));
}
getFilters(pageName: string): FormFilters | null {
const data = sessionStorage.getItem(filters_${pageName});
return data ? JSON.parse(data) : null;
}
}Limitations
- Limited space (5-10MB)
- Only stores text strings (requires serialization/deserialization via JSON.stringify(payload))
- Synchronous data access APIs
- No support for complex queries
- Best for simple objects: The more complex the stored data, the longer serialization/deserialization takes, degrading user experience (because the process is synchronous)
IndexedDB
IndexedDB represents the most advanced solution among client-side storage technologies, offering a true NoSQL database in the browser. Unlike local/session storage, IndexedDB allows us to:
- Save large amounts of structured data
- Use asynchronous data access APIs
- Handle all data insertion through transactions
- Store any data type, including blob storage
Since there are many access APIs, I’ll refer you to the documentation for further details.
In Angular, we can use native APIs to manage IndexedDB, or we can use excellent libraries like Dixie.js or ngx-indexed-db.
Regardless of the chosen library, it’s important to understand when to use this type of storage:
- When I have lots of data to store
- When I have complex data to store
- When I need to execute queries on stored data
Real-World usage examples
- In an application I developed, the user menu was calculated at the backend level. The frontend (Angular), after user login, requested the menu from the backend: once the JSON was obtained, it was stored in an object store (think of it as a database “table”). The Angular store service handled loading from cache or calling the backend API gateway.
- In the same application, each model had a “contract” describing the properties of every field (e.g., whether they were searchable, should be shown in lists, validations, etc.). This configuration was standard for each installation and changed very rarely. Whenever the application interacted with a model, it retrieved the “contract” to apply application logic: the first time it was requested from the backend, but from the second time onward, everything was stored in IndexedDB and information access went only through there. Contract refresh mechanisms were handled via SignalR events from the backend.
Direct comparison: which technology to choose?
| Feature | localStorage | sessionStorage | IndexedDB |
| Persistence | Permanent | Session only | Permanent |
| Storage limit | 5MB | 5-10MB | Hundreds of MB |
| Setup complexity | Simple | Simple | Complex |
| Data types | Strings only | Strings only | Any type |
| API style | Synchronous | Synchronous | Asynchronous |
| Query support | No | No | Yes |
- Choose localStorage if:
• You need persistence across sessions
• Data is simple and limited
• You want quick and direct implementation
• Example: JWT tokens, user preferences, interface theme - Choose sessionStorage if:
• Data is only needed for the current session
• You want to avoid conflicts between different tabs
• Example: wizard progress state, temporary form data, applied filters - Choose IndexedDB if:
• You have large or complex datasets
• You need queries and indexes
• You’re developing advanced offline features
• Example: product catalog synchronization, documents, complex application data
Security considerations
When using client-side storage mechanisms, it’s essential to consider several security aspects:
- Avoid saving personally identifiable information in localStorage/sessionStorage as it will always be visible to the user. However, it won’t be visible to other web apps—each website has access to its own dedicated IndexedDB.
- For encrypted data storage, you’ll need to use Web Crypto APIs and write methods that handle data encryption/decryption (or better yet, use an existing library).
- Always sanitize data before inserting it into the DOM (this is a general best practice, regardless of where the data comes from).
- Use [innerHTML] only when necessary and with sanitization (and leverage Angular’s built-in XSS attack protection).
Conclusions
Choosing the right client-side storage strategy for web applications depends on the specific project requirements. I hope this article has clarified your understanding—the goal is to have a clear picture of what’s available to us.
Remember that a good frontend caching strategy isn’t just about performance, but also about the overall application architecture. By properly integrating these technologies into your application ecosystem, you’ll achieve faster, more responsive web apps that are resilient to connectivity issues.