perf hacks
CachingData Cache

Data Cache

Cache fetch() results and database queries between requests.

Next.js 15 changed caching defaults. In Next.js 14, fetch() is cached by default. In Next.js 15+, fetch() uses no-store by default—you must explicitly opt into caching.

Fetch Caching

In Next.js 14, fetch() is cached by default. In Next.js 15+, you need to explicitly enable caching:

// Next.js 14: Cached automatically
// Next.js 15+: NOT cached by default - use force-cache or revalidate
const data = await fetch('https://api.example.com/data');

Control Duration

// Cache forever (until redeploy)
await fetch(url, { cache: 'force-cache' });

// Cache for 1 hour
await fetch(url, { next: { revalidate: 3600 } });

// Never cache
await fetch(url, { cache: 'no-store' });

Cache Tags

// Tag your fetches
const product = await fetch(`/api/products/${id}`, {
  next: { tags: [`product-${id}`, 'products'] },
});
// Invalidate by tag
import { revalidateTag } from 'next/cache';
revalidateTag('products');

Database Queries

Use unstable_cache for non-fetch data:

import { unstable_cache } from 'next/cache';

const getProduct = unstable_cache(
  async (id: string) => {
    return await db.products.findUnique({ where: { id } });
  },
  ['product'],
  { revalidate: 3600, tags: ['products'] }
);

Request Memoization

Deduplicates identical calls in a single render:

layout.tsx
const user = await getUser(); // Fetch 1

// page.tsx
const user = await getUser(); // Deduplicated!

For non-fetch, use React's cache():

import { cache } from 'react';

export const getUser = cache(async () => {
  return await db.users.findFirst();
});

Common Mistakes

Over-Caching User Data

// ❌ Shared key for all users
const getOrders = unstable_cache(
  async (userId) => db.orders.findMany({ where: { userId } }),
  ['orders'] // Wrong!
);

// ✅ User-scoped key
const getOrders = (userId: string) =>
  unstable_cache(
    async () => db.orders.findMany({ where: { userId } }),
    [`orders-${userId}`],
    { revalidate: 300 }
  )();

Forgetting to Invalidate

// ❌ Cache shows stale data
export async function POST(request: Request) {
  await db.products.create({ data });
  return Response.json({ ok: true });
}

// ✅ Invalidate after mutation
export async function POST(request: Request) {
  await db.products.create({ data });
  revalidateTag('products');
  return Response.json({ ok: true });
}

Key Collisions

// ❌ Same key, different queries
const getActive = unstable_cache(fn1, ['products']);
const getAll = unstable_cache(fn2, ['products']);

// ✅ Unique keys
const getActive = unstable_cache(fn1, ['products', 'active']);
const getAll = unstable_cache(fn2, ['products', 'all']);

With caching set up, see Edge vs Serverless for where to run your code.

On this page