Do you know how to use NextJS caching system?

Updated by Gilles Pothieu [SSW] 4 months ago. See history

123

<introEmbed
  body={<>
Next.js 15 introduced a more explicit and developer-friendly caching system.
But it's not automatic—caching only activates when you *opt in* using three mechanisms: enabling two flags (`dynamicIO`, `useCache`) in your config and adding the `"use cache"` directive inside a file, a component or a function. This triple-lock prevents accidental caching and gives you fine-grained control over revalidation and tagging.
  </>}
/>
## Why it matters

Without this opt-in model, stale content can easily sneak into your UI. Now, you’re in full control. You decide what gets cached, for how long, and what tags are used for easy invalidation.

<youtubeEmbed url="https://www.youtube.com/watch?v=xWkozeculPo" description="Video: Is Next.js 15 any good? "use cache" API first look (8 min)" />

## How to enable caching introduced by Next.js 15

To start using the new cache layer, you need to activate two experimental flags in `next.config.js`, and then opt in at the file or function level using the `"use cache"` directive:

```javascript
// next.config.js
module.exports = {
  experimental: {
    dynamicIO: true,
    useCache: true
  }
}
```

Then, inside your route:

```javascript
// app/products/page.js
'use cache'

export default async function ProductPage() {
  const data = await fetch('/api/products', {
    next: { tags: ['products'] }
  })
  return
}
```

## Caching API options

Use the built-in functions below to fine-tune caching behavior:

- **`cacheLife(seconds)`**: Sets a time-to-live (TTL), similar to ISR’s `revalidate`.
- **`cacheTag(tag)`**: Associates a label (e.g. `user-123`) to allow grouped invalidation.
- **`revalidateTag(tag)`**: Clears all entries with that tag on next request—ideal for post-write updates.

```javascript
cacheLife(3600)
cacheTag(`user-${id}`)
revalidateTag('products')

export async function POST() {
  await updateDB()
  revalidateTag('products')
}
```

## Important changes with `dynamicIO`

When using `dynamicIO`, some core Next.js behaviors change:

1. `cookies()` and `headers()` become async functions:
   ```javascript
   import { cookies } from 'next/headers'
   const token = (await cookies()).get('token')
   ```

2. Route files are dynamic by default. You must explicitly add:
   ```javascript
   export const dynamic = 'force-static'
   ```
   if you want to guarantee build-time rendering.

## Pro Patterns

Use hybrid caching for best results:

```javascript
'use cache'
export default async function Dashboard() {
  const staticData = await fetch('/api/metrics', { cacheLife: 3600 })
  const liveData   = await fetch('/api/activity', { cache: 'no-store' })
  return
}

cacheTag(`user-${crypto.randomUUID()}`)
```

<figureEmbed figureEmbed={{
  preset: "goodExample",
  figure: 'Figure: Good example - Mixing real-time and cached content gives you performance *and* freshness',
  shouldDisplay: true
} } />


## Additional Notes

- Cache keys are based on build ID + args, so new props auto-generate new cache entries.
- The client router cache uses `staleTime: 0` by default—navigations recheck the server.
- Deploys automatically clear cache, so no need for manual flushing.

You can learn more in depth about how to cache with Next.js in the [official documentation](https://nextjs.org/docs/app/building-your-application/caching).

## Common Pitfalls

Avoid caching components that depend on non-serializable props (e.g. `children`):

```javascript
'use cache'
function SafeCache({ children }) {
  return <section>{children}</section>
}
```

## Debugging Tips

To inspect what was cached or missed:

```bash
NEXTJS_CACHE_ANALYTICS=1 next build
```

This emits a hit/miss report for each route to the console, giving visibility into caching behavior.

Acknowledgements

Gilles Pothieu
Related rules

Need help?

SSW Consulting has over 30 years of experience developing awesome software solutions.

We open source.Loving SSW Rules? Star us on GitHub. Star