Dedupe and preload fetches
Wrap data functions in cache() and start them early.
From next-best-practices, data-patterns.md and metadata.md.
One query, two callers
Render twice, then tick the object argument.
Plain async functionBad
const getPost = async (slug) => …Wrapped in cache()Good
const getPost = cache(async (slug) => …)// lib/posts.ts
import { cache } from 'react'
export const getPost = cache(async (slug: string) => {
return db.posts.findFirst({ where: { slug } })
})
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const { slug } = await params
const post = await getPost(slug)
return { title: post.title }
}
export default async function Page({ params }) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.body}</article>
}
Preload before the other work
Press Play, then drag both sliders.
Await after the workBad
work + querypreloadUser(id) firstGood
max(work, query)// lib/data.ts
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } })
})
export const preloadUser = (id: string) => {
void getUser(id)
}
// app/user/[id]/page.tsx
export default async function UserPage({ params }) {
const { id } = await params
preloadUser(id)
await doOtherWork()
const user = await getUser(id)
return <div>{user.name}</div>
}
Each component fetches its own
Toggle cache() and add components.
Server component treeBad
0
DB queries
async function Avatar({ id }: { id: string }) {
const user = await getUser(id)
return <img src={user.avatarUrl} alt="" />
}
Why it matters, pros and cons
Why it matters
cache() gives every caller one promise per request; a preload starts it early.
Pros
- One query per argument per request.
- Components fetch their own data, no drilling.
- Latency overlaps work you already do.
Cons
- Scoped to one request; use
'use cache'across requests. - Object arguments are a new key every call.
- GET
fetch()is already memoized; wrap DB calls.