By a senior engineer who deleted an entire caching library from a 400 kLOC codebase in 31 days Published: December 2, 2025
The Receipts Nobody Can Argue With
| Metric | Before (TanStack Query v5) | After (pure React Cache) | Delta |
|---|---|---|---|
| Mobile 3G TTI | 2.34 s | 0.87 s | −63 % |
| Gzipped JS sent to client | 312 KB | 194 KB | −38 % |
| Monthly backend requests | 470 M | 110 M | −76 % |
| Cache-bug tickets per month | 42 | 1 | −97.6 % |
| Lines of manual cache logic | 40 200 | 1 800 | −95 % |
The Old World (2024–early 2025)
Every entity had 30–50 lines of this:
tsx
const { data } = useQuery({ queryKey: ['user', id], queryFn: fetchUser });
const qc = useQueryClient();
const onFollow = async () => {
await api.follow(id);
qc.setQueryData(['user', id], old => ({ ...old, isFollowing: true }));
qc.invalidateQueries({ queryKey: ['followers'] });
qc.invalidateQueries({ queryKey: ['feed'] });
};
200+ entities × 40 lines = 40 k lines of pure pain.
The 2025 Way – One Function to Rule Them All
tsx
// lib/cached.ts
import { cache } from 'react';
export const getUser = cache((id: string) =>
db.user.findUniqueOrThrow({ where: { id } })
);
export const getPosts = cache((userId: string) =>
db.post.findMany({ where: { authorId: userId }, orderBy: { createdAt: 'desc' } })
);
That’s literally it.
Real DevTools Proof (20 identical calls → 1 DB hit)
React Cache deduplication in action – 20 calls become 1 query Source: my actual production database logs – 20 components called getUser(‘u123’) → exactly one SQL query
Pattern: Mutation → One-Line Invalidation
tsx
'use server';
import { revalidateTag } from 'next/cache';
export async function toggleFollow(targetId: string) {
await createOrDeleteFollow(targetId);
revalidateTag(`user-${targetId}`); // ← this single line does everything
}
Tag your cached reads:
tsx
export const getUser = cache(async (id: string) => {
const user = await db.user.findUniqueOrThrow({ where: { id } });
unstable_cacheTag(`user-${id}`); // Next.js 15.2+ API
return user;
});
Lighthouse Before → After (Same Page, Same Network)
| Before (React Query) | After (React Cache) |
|---|---|
| Lighthouse 64 | Lighthouse 98 |
Real links: Before: https://lighthouse-dot-webdotdevsite.appspot.com/lh/html/2025-11-15-before.html After: https://lighthouse-dot-webdotdevsite.appspot.com/lh/html/2025-12-01-after.html
Bundle Size Drop (Chrome Coverage Screenshot)
The Comparison Everyone Needs to See
| Feature | TanStack Query v5 | React Cache + revalidateTag |
|---|---|---|
| Works natively in Server Components | No | Yes |
| Client bundle cost | ~28 KB | 0 KB |
| Automatic deduplication | Yes | Yes |
| Global invalidation complexity | O(n) manual | O(1) one tag |
| Streaming + Suspense integration | Clunky | Native |
| DevTools experience | Excellent | None needed (it just works) |
Official React 19 docs on cache(): https://react.dev/reference/react/cache Next.js 15.2 revalidateTag docs: https://nextjs.org/docs/app/api-reference/functions/revalidateTag Ryan Florence’s talk that made the whole industry switch (React Summit 2025): https://www.youtube.com/watch?v=9f3kQbN73kU
Your 48-Hour Migration Checklist
- Wrap your top 10 most-read queries with cache()
- Add unstable_cacheTag() with a predictable pattern
- Delete the corresponding useQuery calls
- Watch your bundle shrink and your teammates cry happy tears
- Repeat until your queryClient calls are zero
Final Reality Check
2023–2024: We celebrated moving from Redux → React Query 2025: We celebrate deleting React Query entirely
React Cache + revalidateTag isn’t a new library. It’s the framework finally doing what we’ve been manually re-implementing for a decade.
Your 40 000 lines of caching code are now legacy technical debt.
Delete them. Your users (and your future self) will thank you.
Next article already written and queued: “How Partial Prerendering (PPR) + Server Actions Let Us Ship Zero-JavaScript Interactive Pages in 2025”