Why Your Next.js Data Fetching Might Be Slower Than You Think
I shipped a page last month that felt fast locally but crawled in production. Turns out I was making three sequential API calls when I could've made one. Then I wasn't caching. Then I was re-validating static content every request.
Sound familiar? Here's what I learned.
The waterfall trap
You fetch User → then fetch Posts → then fetch Comments. Each waits for the previous one to finish. Three API calls that could run in parallel are now running serially. That's a waterfall.
// ❌ Serial fetches = slow
export default async function Page({ params }) {
const user = await fetch(`/api/users/${params.id}`);
const posts = await fetch(`/api/posts?userId=${user.id}`);
const comments = await fetch(`/api/comments?postId=${posts[0].id}`);
return <div>User: {user.name}, Posts: {posts.length}, Comments: {comments.length}</div>;
}If each fetch takes 200ms, you're at 600ms. That adds up fast.
The fix: Fetch in parallel.
// ✅ Parallel fetches = fast
export default async function Page({ params }) {
const [user, posts] = await Promise.all([
fetch(`/api/users/${params.id}`),
fetch(`/api/posts?userId=${params.id}`),
]);
const comments = await fetch(`/api/comments?postId=${posts[0].id}`);
return <div>User: {user.name}, Posts: {posts.length}, Comments: {comments.length}</div>;
}Now user and posts fetch together. Comments depends on posts, so it waits. Total time: ~400ms instead of 600ms. That's real.
Missing cache headers
You deployed a static page (or it should be static). But your API doesn't set cache headers, so Next.js re-fetches from your origin on every request. Your CDN can't cache it.
// ❌ No cache headers = re-fetching everything
export default async function Page() {
const data = await fetch(`/api/products`);
return <div>{/* render products */}</div>;
}Each visitor re-hits your database. Your server starts sweating.
The fix: Add cache headers to your API, and tell Next.js to cache the fetch.
// ✅ Cache headers + Next.js config
export default async function Page() {
const data = await fetch(`/api/products`, {
next: { revalidate: 3600 }, // cache for 1 hour
});
return <div>{/* render products */}</div>;
}And on your API route:
export async function GET() {
const products = await db.getProducts();
return Response.json(products, {
headers: {
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
},
});
}Now your CDN caches it. Your origin takes 1/100th the requests. Your database can breathe.
Generating static pages when you can
Not every page needs to be dynamic. If your product listing doesn't change every minute, generate it at build time (or at request time and cache it).
// ❌ Dynamic rendering on every request (even if data doesn't change)
export default async function Page() {
const products = await fetch(`/api/products`);
return <div>{/* render */}</div>;
}The fix: Use generateStaticParams for predictable routes and ISR for the rest.
// ✅ Static generation + revalidation
export const revalidate = 3600; // revalidate every hour
export async function generateStaticParams() {
const categories = await db.getCategories();
return categories.map((cat) => ({ category: cat.slug }));
}
export default async function Page({ params }) {
const products = await fetch(`/api/products?category=${params.category}`, {
next: { revalidate: 3600 },
});
return <div>{/* render */}</div>;
}Now Next.js generates those pages once (or every hour). Visitors get static HTML. Instant.
Profile before you optimize
I spent an hour optimizing the wrong thing last week. Check where the time actually goes:
# Use Next.js build output to see which routes are slow
npm run buildLook at the build output. See Static vs Dynamic vs ISR. If something's dynamic that shouldn't be, fix it.
Use DevTools Network tab in production (or your analytics). Where are users waiting?
The real wins
- Kill waterfalls:
Promise.all()for parallel fetches. - Cache headers: Set them on your API. Let CDNs and Next.js cache.
- Static where possible: If data doesn't change every request, don't fetch every request.
- Measure: Profile before you panic.
I saved 300ms on one page just by parallelizing three fetches. Another 200ms by adding cache headers. That's half a second that users feel.
Next.js is already fast. These patterns just stop you from accidentally making it slow.
— Mustaque