Browser caching, the kind we just covered, handles static files beautifully. CSS, JavaScript bundles, images. Set the right headers and the browser takes care of the rest.
But what about dynamic data? API responses. User profiles. Shopping cart contents. A list of search results the user just fetched 10 seconds ago. The HTTP cache doesn't handle these well, because most API responses are served with no-cache or short TTLs. And even when an API response is cached at the HTTP level, your application often needs that data in a structured, queryable format, not as raw HTTP response bytes.
That's where client-side comes in. It's the practice of storing data inside your application's runtime, in JavaScript objects, in state management libraries, in IndexedDB or localStorage, so your app can access it instantly without making another network request.
Every modern single-page application does this, whether developers realize it or not. React Query caches API responses. Redux stores application state. Apollo Client caches query results. Zustand holds UI state. All of these are forms of client-side caching.
Client-side isn't one thing, it's a stack of different storage mechanisms, each with different trade-offs.
In-memory (JavaScript runtime) The fastest option. Data lives in variables, objects, Maps, or state management stores (Redux, Zustand, TanStack Query). Access is instant, nanoseconds. But it all vanishes when the user refreshes the page or closes the tab.
SessionStorage Key-value storage scoped to a single browser tab. Survives page refreshes within that tab, but dies when the tab closes. Limited to about 5 MB per origin. Stores strings only, you have to JSON.stringify everything.
LocalStorage Same as SessionStorage but persists across tab closures and browser restarts. Good for preferences, tokens, and small datasets. Still limited to about 5 MB, still strings only, still synchronous (blocks the main thread on read/write).
IndexedDB The heavy hitter. A full database running in the browser. Supports structured data, indexes, transactions, and can store hundreds of megabytes. Asynchronous, so it doesn't block the UI. Overkill for simple caching, but essential for offline-first apps.
| Storage | Capacity | Persistence | Speed | Use Case |
|---|---|---|---|---|
| In-memory | Limited by RAM | Tab lifetime | Nanoseconds | API response caching, UI state |
| SessionStorage | ~5 MB | Tab lifetime | Microseconds | Form drafts, temp state |
| LocalStorage | ~5 MB | Permanent | Microseconds (sync) | Preferences, tokens |
| IndexedDB | 100+ MB | Permanent | Milliseconds (async) | Offline data, large datasets |
The pattern that's taken over the frontend world is stale-while-revalidate. Libraries like TanStack Query, SWR, and Apollo Client all implement it.
Here's how it works:
The user sees data immediately (fast), and then sees it refresh smoothly if anything changed (accurate). It's the best of both worlds.
What this looks like in practice:
You navigate to your dashboard. The notification count shows "7" instantly from cache. Half a second later, it silently updates to "9" because two new notifications arrived. You barely notice the update. But without client-side , you'd see a loading spinner for 300ms before seeing any number at all. That loading spinner is the difference between an app that feels native and one that feels like a website.
The consistency problem:
Client-side caches can get out of sync. User opens two tabs. Tab 1 marks a notification as read. Tab 2 still shows it as unread. This is the same problem we saw with local caches on the server side, each client has its own independent copy. Solutions include -based real-time updates, or simply accepting eventual consistency with short cache TTLs.
You've heard the theory, show cached data instantly, refresh in the background. But what does that actually look like as a request flow? This diagram walks you through a stale-while-revalidate cycle from the moment a user triggers a fetch to the background refresh that silently updates the cache.
Not all client-side caches are created equal. Your browser gives you at least four different storage mechanisms, each with different speed, capacity, and persistence trade-offs. This layered view shows how they stack, from nanosecond in-memory state at the top all the way down to IndexedDB at the bottom. Each layer is slower but more durable.
3 questions - Score 80% to pass
What's the main advantage of the stale-while-revalidate pattern?
You need to cache 50 MB of map tile data for an offline-capable mobile web app. Which storage mechanism should you use?
A user opens your app in two tabs. In Tab 1, they delete a to-do item. Tab 2 still shows the item. Why?