Ensure 'use cache' is used strategically to minimize CPU usage and ISR writes. Use when creating/modifying queries to verify caching decisions align with data update patterns and cost optimization.
This Skill ensures strategic use of Next.js 16 Cache Components to minimize CPU usage and ISR write overhead while maximizing cost efficiency.
NOT every query needs "use cache". Apply caching only when:
✅ Good Caching Candidates:
❌ Poor Caching Candidates:
Why Strategic Caching Matters:
Without caching:
With well-planned caching (cacheLife("max") with 30-day revalidation):
revalidateTag())With poorly-planned caching (short revalidation periods, granular tags):
Before adding "use cache", ask:
import { CACHE_TAG } from "@web/lib/cache";
import { cacheLife, cacheTag } from "next/cache";
export const getCarRegistrations = async () => {
"use cache";
cacheLife("max"); // 30-day revalidation for monthly data
cacheTag(CACHE_TAG.CARS); // Domain-level tag, NOT per-query
return db.query.cars.findMany({
// ... query logic
});
};
Use domain-level tags (from src/lib/cache.ts):
CACHE_TAG.CARS - All car registration queriesCACHE_TAG.COE - All COE bidding queriesCACHE_TAG.POSTS - All blog post queriesWhy domain-level?
revalidateTag(CACHE_TAG.CARS)car-${make}-${year} (excessive ISR writes)Project uses custom "max" profile (next.config.ts):
cacheLife: {
max: {
stale: 2592000, // 30 days - client cache
revalidate: 2592000, // 30 days - automatic regeneration
expire: 31536000, // 1 year - cache expiration
},
}
When to use cacheLife("max"):
When NOT to use caching:
export const getLatestCOE = async (): Promise<COEResult[]> => {
"use cache";
cacheLife("max"); // Monthly updates = perfect fit
cacheTag(CACHE_TAG.COE);
return db.query.coe.findFirst({
orderBy: desc(coe.month),
});
};
Why this works: COE data updates 2x/month, 30-day cache = ~2 regenerations/month.
// DON'T DO THIS
export const getUserPreferences = async (userId: string) => {
"use cache"; // ❌ Wrong! User-specific data shouldn't be cached globally
cacheLife("max");
cacheTag(CACHE_TAG.USERS);
return db.query.users.findFirst({ where: eq(users.id, userId) });
};
Why this fails: Each user needs their own data, global caching creates stale/wrong results.
// DON'T DO THIS
export const getBlogViewCount = async (postId: string) => {
"use cache"; // ❌ Wrong! View counts change on every page view
cacheLife("max");
cacheTag(CACHE_TAG.POSTS);
return db.query.analytics.count({ where: eq(analytics.postId, postId) });
};
Why this fails: 30-day cache on data that changes every minute = stale data.
export const createPost = async (data: PostInput) => {
// NO "use cache" - write operations should never be cached
const result = await db.insert(posts).values(data);
// Invalidate cache AFTER write
revalidateTag(CACHE_TAG.POSTS);
return result;
};
Prefer manual revalidation over automatic:
// In API route or workflow after data import
import { revalidateTag } from "next/cache";
import { CACHE_TAG } from "@web/lib/cache";
// After monthly LTA data import completes
revalidateTag(CACHE_TAG.CARS); // Immediate cache refresh
revalidateTag(CACHE_TAG.COE);
Benefits:
When reviewing query functions, verify:
cacheLife, cacheTag from next/cache, CACHE_TAG from @web/lib/cachecacheLife("max") for monthly dataCACHE_TAG.*, not granular per-query tagssrc/queries/cars/ - Car registration queriessrc/queries/coe/ - COE bidding queriessrc/queries/logos/ - Logo fetching queriessrc/app/blog/_queries/)Strategic Caching (monthly data with 30-day revalidation):
Over-Caching (caching everything with short revalidation):
Under-Caching (no caching at all):
apps/web/CLAUDE.md (Cache Components & Optimization section)next.config.ts (cacheLife profile)src/lib/cache.ts (CACHE_TAG constants)/vercel/next.js