Our app was getting slower on reload. Not dramatically — just that familiar sluggishness we'd always chalked up to network latency. Then one day I actually profiled a page reload and saw the real cause: the entire Apollo cache was being loaded into memory at startup, before a single component had rendered.
Data from a dozen different sections of the app — all of it dumped into memory at time zero. The user had just logged in. The homepage needed none of it.
I was already using apollo-cache-persist — a great library that does exactly what it says. The problem wasn't the library, it was the persistence model itself. So I used it as a baseline and built a different approach on top of it: apollo-lazy-cache-persist.
The Problem with Traditional Apollo Cache Persistence
apollo-cache-persist works by serializing the entire InMemoryCache and saving it to storage. On startup, it reads that snapshot back and restores everything at once. For small apps, this is fine. For larger apps — SaaS dashboards, enterprise tools, anything with lots of interconnected data — it becomes a silent performance killer.
Two problems compound each other:
- Startup does too much work. Every entity in your cache — regardless of whether the current page needs it — gets hydrated into memory immediately. The bigger your cache grows, the worse your startup gets.
- The storage model keeps growing. The entire cache is stored as a single monolithic blob. Every page you visit, every query that runs, makes that blob larger. Pagination queries are especially bad: every new page of results gets merged in, and the blob keeps growing session after session. Left unchecked, it balloons.
The Lazy Approach: Only Load What's Needed, When It's Needed
The insight was simple: you don't need the whole cache at startup. You need the queries that the current page is actually running.
Instead of restoring a monolithic snapshot, apollo-lazy-cache-persist works as an Apollo Link that intercepts each query as it fires:
Query fires
↓
Check IndexedDB for this specific query key
↓
If found → write result into InMemoryCache immediately (UI renders instantly)
↓
Network request continues in the background
↓
Fresh network response arrives → update InMemoryCache + re-persist this query
Each query is stored as its own independent entry, keyed by operation name + variables. Pagination queries are automatically skipped — variables containing cursor, offset, after, before, first, or last are excluded, so you never accumulate stale partial result sets.
The result: cache entities load incrementally as queries resolve, not all at once. The app starts with only what the current page needs, and grows from there.
How to Use It
npm install apollo-lazy-cache-persist
import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from "@apollo/client"
import localforage from "localforage"
import { createLazyCacheStore, createLazyCacheLink } from "apollo-lazy-cache-persist"
const cache = new InMemoryCache()
const storage = localforage.createInstance({
name: "apollo-lazy-cache-persist",
storeName: "query_cache"
})
const store = createLazyCacheStore({
storage,
ttl: 7 * 24 * 60 * 60 * 1000 // 7 days
})
const lazyLink = createLazyCacheLink({ cache, store })
const client = new ApolloClient({
cache,
link: ApolloLink.from([
lazyLink,
new HttpLink({ uri: "/graphql" })
])
})
That's the entire integration. No changes to your queries, no new hooks, no wrappers around your app.
For React Native, swap localforage for AsyncStorage:
import AsyncStorage from "@react-native-async-storage/async-storage"
const store = createLazyCacheStore({
storage: AsyncStorage,
ttl: 7 * 24 * 60 * 60 * 1000
})
What It Doesn't Do (By Design)
This package intentionally does not persist the entire Apollo cache. Manual cache writes like cache.writeQuery, cache.modify, or cache.writeFragment are not persisted.
This is a deliberate tradeoff: by only persisting network responses, the system stays predictable. You never get stale optimistic updates or inconsistent derived data coming back from storage on reload.
If you rely heavily on client-side cache mutations that need to survive refreshes, the original apollo-cache-persist is probably the better fit. But if your app is primarily reading from the network and you want faster, leaner startup — this is what this package was built for.
Comparison
| Feature | apollo-lazy-cache-persist | apollo-cache-persist |
|---|---|---|
| Startup memory usage | Low — only active queries | High — entire cache |
| Cache load pattern | Incremental | All at once |
| Storage model | Per-query entries | Single blob |
| Pagination data stored | No (auto-skipped) | Yes |
| Manual cache writes persisted | No | Yes |
When Should You Use This?
- Your Apollo cache grows large over a session
- Startup performance or time-to-interactive matters to you
- You're building a React Native app where memory pressure is real
- You're on a SaaS dashboard or enterprise app with many query types per session
Links
Big thanks to the apollo-cache-persist maintainers whose work this builds on. Feedback and contributions very welcome — especially benchmarks from React Native environments where this pattern tends to shine most.
Top comments (0)