I wrote earlier about cache() fixing redundant database queries when multiple components in a tree independently call the same query function. There's one specific, extremely common place this exact pattern shows up that's worth calling out directly, because it's not an edge case, it's the default shape of nearly every dynamic page with SEO metadata.
The Setup Almost Every Dynamic Page Has
// app/blog/[slug]/page.tsx
import { getPost } from '@/lib/queries/posts';
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug); // query #1
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPost({ params }) {
const { slug } = await params;
const post = await getPost(slug); // query #2, same post, same request
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
This is the completely standard pattern from the Next.js metadata setup covered in an earlier post, and it's genuinely necessary, generateMetadata needs the post's title and excerpt for the meta tags, the page component needs the post's full content to actually render it. Both run for every single request to this page. Both call getPost(slug) independently. Without a specific fix, that's two full database round trips for what is, semantically, the exact same piece of data, fetched twice, for every single page view.
Why This Specific Case Matters More Than It Might Seem
This isn't a rare pattern someone might occasionally write, it's the default shape of generateMetadata combined with any dynamic page, which means it's quietly doubling database load on nearly every content-driven page across an entire site, blog posts, product pages, any dynamically rendered detail page with SEO metadata. A site with meaningful traffic on these pages is paying for twice the database queries it actually needs, on some of its most frequently visited pages, for no functional reason.
The Fix: Wrap the Query in React's cache()
// lib/queries/posts.ts
import { cache } from 'react';
import { connectDB } from '@/lib/db';
import Post from '@/models/Post';
export const getPost = cache(async (slug: string) => {
await connectDB();
return Post.findOne({ slug }).lean();
});
That's the entire fix, no changes needed anywhere else. generateMetadata and the page component both still call getPost(slug) exactly as before, the code looks identical. What changes is that React's request memoization now recognizes both calls, same function, same argument, within the same render pass, and only actually executes the underlying query once, returning the cached result for the second call.
Why This Is Worth Checking Specifically, Not Just Generally
The earlier post on this topic covered the general pattern, multiple components in a tree calling the same query. This specific case is worth checking on its own because it's so easy to miss precisely because generateMetadata and the page component don't look like they're part of the same "component tree" in the way nested components obviously are. They're two separate exported functions in the same file, and it's genuinely easy to write both, each looking completely correct in isolation, without ever noticing they're duplicating a database call for the same request.
A Quick Way to Verify This Is Actually Happening
Add a temporary log inside the un-cached query function and load the page once:
export async function getPost(slug: string) {
console.log('getPost called for:', slug); // temporary
await connectDB();
return Post.findOne({ slug }).lean();
}
If you see that log line twice for a single page load, that's this exact issue, confirmed directly rather than assumed. After wrapping the function in cache(), the same test should show it logging only once per request.
Where This Doesn't Apply
If generateMetadata and the page component genuinely need different data, metadata pulling from a lighter, separate summary endpoint while the page fetches full content from somewhere else entirely, there's no duplication to fix, since they're not actually calling the same underlying function with the same arguments in the first place. This fix specifically applies when both are calling the identical query function, which, for most blog and content-driven page setups, is exactly the common case.
The Actual Rule
Any query function called both inside generateMetadata and inside the page component it belongs to should be wrapped in cache(), as close to a universal default as a rule gets in this specific area. It costs nothing to add, changes no calling code, and directly eliminates a real, silent, doubled database query that exists by default on essentially every dynamically rendered page with SEO metadata.
Go check any page in your own project using generateMetadata alongside a matching page component, specifically whether the underlying query function is wrapped in cache(). If it's not, you're very likely double-querying on every single page load for that route. Drop what you find in the comments.
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)