Reduce redundant requests, boost performance, and make your APIs feel instant with these five practical memoization strategies for modern apps.

Introduction
Let’s be honest, most JavaScript apps waste bandwidth.
Your frontend probably calls the same endpoint multiple times:
/api/users
→ once on load/api/users
→ again, when a component re-renders/api/users
→ once more after a route change
Same URL. Same data. Same payload.
That’s redundant work for your app and your backend.
The fix? Memoization is caching the results of a function call so identical calls can return instantly without hitting the network again.
In this post, we’ll explore five smart ways to memoize expensive API calls, from simple in-memory solutions to advanced TTL and persistent caching.
1. The Classic In-Memory Cache
This is the simplest and fastest method to store API responses in memory and reuse them for repeated requests.
const memoizedFetch = (() => {
const cache = new Map();
return async function (url, options = {}) {
const key = url + JSON.stringify(options);
if (cache.has(key)) {
console.log("Cache hit for", url);
return cache.get(key);
}
console.log("Fetching fresh data:", url);
const response = await fetch(url, options);
const data = await response.json();
cache.set(key, data);
return data;
};
})();
✅ Pros
- Dead simple to implement
- Instant lookups
- Great for single-page apps
⚠️ Cons
- Cache resets on reload
- No expiration control
- Not ideal for long-lived data
📦 Use it for: Quick wins in frontend apps (e.g., React dashboards).
2. Memoization with Expiration (TTL-Based Caching)
Sometimes you want fresh data eventually, but not on every call.
Add a time-to-live (TTL) mechanism to expire cache entries after a specific duration.
const memoizedFetchWithTTL = (() => {
const cache = new Map();
const TTL = 60 * 1000; // 1 minute
return async (url) => {
const now = Date.now();
const cached = cache.get(url);
if (cached && now - cached.timestamp < TTL) {
console.log("Cache hit:", url);
return cached.data;
}
console.log("Fetching fresh:", url);
const res = await fetch(url);
const data = await res.json();
cache.set(url, { data, timestamp: now });
return data;
};
})();
✅ Pros
- Keeps data fresh
- Automatic invalidation
- Easy to reason about
⚠️ Cons
- Time-based, not event-based
- All clients share the same TTL logic
📦 Use it for: APIs that update periodically (weather, analytics, metrics).
3. Deduplicating In-Flight Requests
Here’s a subtle but real-world issue:
If multiple components request the same data at once, they all trigger separate fetches.
You can fix that by memoizing in-flight Promises that return the same Promise until it resolves.
const memoizedInFlightFetch = (() => {
const cache = new Map();
const inFlight = new Map();
return async (url) => {
if (cache.has(url)) return cache.get(url);
if (inFlight.has(url)) return inFlight.get(url);
const promise = fetch(url)
.then((res) => res.json())
.then((data) => {
cache.set(url, data);
inFlight.delete(url);
return data;
})
.catch((err) => {
inFlight.delete(url);
throw err;
});
inFlight.set(url, promise);
return promise;
};
})();
✅ Pros
- Prevents duplicate simultaneous requests
- Saves bandwidth
- Works great with React concurrent rendering
⚠️ Cons
- Needs proper cleanup
- Slightly more complex logic
📦 Use it for: Dashboards or pages with multiple components fetching the same resource simultaneously.
4. Persistent Caching (localStorage or IndexedDB)
Want to cache results across sessions? Store them persistently.
Using localStorage
:
const persistentFetch = async (url) => {
const cached = localStorage.getItem(url);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < 300000) { // 5 min TTL
console.log("Loaded from localStorage");
return data;
}
}
console.log("Fetching new data...");
const res = await fetch(url);
const data = await res.json();
localStorage.setItem(url, JSON.stringify({ data, timestamp: Date.now() }));
return data;
};
✅ Pros
- Survives reloads & sessions
- Great for read-heavy apps
⚠️ Cons
- Limited to ~5MB
- Must serialize/deserialize
- Not suitable for sensitive or frequently changing data
For larger, structured storage, use IndexedDB (via libraries like idb-keyval
).
📦 Use it for: Offline-ready apps, PWA dashboards, or public data caching.
5. Using Libraries: SWR / React Query / TanStack Query
If you’re using React or a modern frontend stack, you don’t need to reinvent the wheel.
Libraries like SWR (from Vercel) and TanStack Query (React Query) provide built-in memoization, deduplication, revalidation, and persistence.
Example with SWR:
import useSWR from "swr";
const fetcher = (url) => fetch(url).then((r) => r.json());
export default function Profile() {
const { data, error, isLoading } = useSWR("/api/user", fetcher, {
revalidateOnFocus: false,
dedupingInterval: 60000, // 1 min
});
if (error) return <div>Error loading</div>;
if (isLoading) return <div>Loading...</div>;
return <div>Hello, {data.name}</div>;
}
✅ Pros
- Automatic deduping
- Stale-while-revalidate caching
- Handles refetching and invalidation intelligently
⚠️ Cons
- Framework-specific
- Less control over low-level logic
📦 Use it for: Any production React app. It’s the de facto memoization solution.
Bonus: Hybrid Approach (Smart + Manual)
In production, the best strategy often mixes multiple methods:
- Use in-flight caching to prevent duplicate concurrent calls
- Add a TTL layer for short-term freshness
- Persist results in localStorage for long-term caching
- Fall back to SWR/React Query for automated revalidation
This gives you the perfect balance between performance, accuracy, and simplicity.
Gotchas to Watch Out For
- Avoid memoizing POST/PUT/DELETE; they cause side effects.
- Be careful with auth tokens; different users need different caches.
- Clean up memory use TTL or LRU (least recently used) strategies for large caches.
- Invalidate smartly after mutations, force a refetch, or clear relevant cache entries.
- Use parameter-aware keystrokes that include query params and headers in cache keys.
Why Memoizing API Calls Matters in 2025
Modern apps rely on dozens (sometimes hundreds) of microservices.
Each network request adds latency, power consumption, and cost.
Memoization isn’t just optimization, it’s sustainability.
It makes your app feel instant, your servers lighter, and your UX smoother.
✅ Key takeaway: Don’t refetch what you already know, let your code remember.
Conclusion
Memoizing API calls is one of the simplest, most impactful optimizations in modern JavaScript.
Whether you use a lightweight in-memory cache or full-blown React Query, these techniques will help you:
- Reduce redundant requests
- Speed up load times
- Improve user experience
- Lower API costs
🚀 5 Smart Ways Recap:
- In-memory cache
- TTL-based caching
- In-flight deduplication
- Persistent storage (localStorage / IndexedDB)
- Framework-level memoization (SWR / React Query)
Call to Action
Have you implemented API memoization in your apps or found your own caching strategy?
Share your approach in the comments 👇
And if your teammate’s app keeps calling the same endpoint five times per render, send them this post. It might just save their API budget.
Leave a Reply