Next.js build taking too long on a Sanity site: causes and the fix
By Nayan Kyada · · 6 min read
Part of The Sanity + Next.js Guide
Your Next.js build is taking too long when a deploy that once finished in a couple of minutes now runs for fifteen, twenty, or longer, and a content change or a one-line fix sits in a queue. On Sanity sites this is almost always self-inflicted: the build is doing the work of rendering and fetching every page in the dataset, every time. Here is how I diagnose it, what it costs you to ignore, and the order I fix it in.
Symptoms that point to a build problem
Check whether these sound familiar:
- Deploy time grows every time the editors publish more content.
- The build log shows the same page count climbing, with long gaps between prerendered routes.
- Builds occasionally fail with timeouts or fetch errors from the Sanity API, then pass on retry.
- Editors ask whether the site is broken because they cannot see their change for ages.
- Your hosting plan reports a lot of build minutes, and a preview deploy per pull request multiplies it.
If build time scales with content count, the cause is structural, not a bad server. Throwing a bigger build machine at it helps a little and hides the real issue.
What ignoring it costs
The direct cost is build minutes, which is a line item on your hosting bill (see my breakdown of what drives a Vercel bill on a Next.js site). The bigger cost is behavioural. When deploys are slow, developers batch changes, hotfixes wait, and marketing stops trusting that publishing will show up. A site where nobody wants to deploy is a site that rots.
Cause 1: prerendering every document on every build
The most common pattern I find is a dynamic route that returns every slug from generateStaticParams. With 40 pages that is fine. With 4,000 it is the entire build.
// app/blog/[slug]/page.tsx (broken: prerenders everything)
import {client} from '@/sanity/lib/client'
export async function generateStaticParams() {
const slugs = await client.fetch<string[]>(
`*[_type == 'post' && defined(slug.current)].slug.current`
)
return slugs.map((slug) => ({slug}))
}The fix is to prerender only what matters and let the rest build on demand. Pages not returned from generateStaticParams are rendered on first request and then cached, because dynamicParams defaults to true.
// app/blog/[slug]/page.tsx (fixed: top pages only, rest on demand)
import {client} from '@/sanity/lib/client'
export const dynamicParams = true
export async function generateStaticParams() {
const slugs = await client.fetch<string[]>(
`*[_type == 'post' && defined(slug.current)]
| order(publishedAt desc)[0...50].slug.current`
)
return slugs.map((slug) => ({slug}))
}Fifty of the newest posts is a starting point, not a rule. Use your analytics: prerender the pages that actually receive traffic. I use the same approach in the 100+ post blog build.
Cause 2: every page fetches far more than it renders
The second pattern is a page query that pulls whole documents, dereferences everything, and returns fields the template never touches. Multiply that payload by thousands of pages and the build spends its life waiting on the network.
Open your page queries and look for a bare projection that returns everything, or deep reference expansion several levels down. Replace it with an explicit projection of the fields the page uses. The techniques in advanced GROQ queries and the conditional projections post cover this in detail.
Cause 3: the same data fetched many times
Layouts, headers, footers, and navigation often each call Sanity separately, and so does every page that needs site settings. Some of these are deduplicated within a render, some are not, depending on how you fetch. A site with a navigation query, a settings query, and a footer query on each of 2,000 pages is 6,000 requests before the page content itself.
Give shared data one fetch function with explicit caching, and tag it so you can invalidate it precisely:
// sanity/lib/fetch.ts
import {client} from './client'
export async function sanityFetch<T>(
query: string,
params: Record<string, unknown> = {},
tags: string[] = []
): Promise<T> {
return client.fetch<T>(query, params, {
next: {revalidate: 3600, tags},
})
}Then call it with a tag such as settings for global data. Pair it with webhook-driven invalidation, as described in batching tags on Sanity webhooks, so a one-hour fallback is only a safety net.
Cause 4: the build is not using the CDN
For published content during a build, the Sanity API CDN is usually the right choice. If your client is configured with the CDN turned off everywhere, builds hit the live API directly and are more exposed to rate limits, which explains the random failures that pass on retry. Keep the CDN on for published reads and turn it off only for draft or preview fetches. Confirm the exact behaviour in your next-sanity client config and the Sanity docs for your version.
Cause 5: heavy work done at build time
Look for things that do not belong in the build at all: generating image variants for every asset, rendering OG images for every page ahead of time, or running a search index sync inside the build step. Move these to a webhook-triggered job or generate them lazily on request. A build should render pages, not run a content pipeline.
Fix order I use
- Measure first: note total build time and page count from a recent deploy log.
- Cap
generateStaticParamsto the pages that matter. This is usually the biggest single win. - Trim page queries to explicit projections.
- Centralise shared fetches with tags and a revalidate window.
- Check CDN usage and rate-limit errors.
- Move non-rendering work out of the build.
Do them in that order, rebuilding after each, so you know which change paid off.
How to verify the fix
Compare build duration and the number of statically generated routes before and after. Then visit a page you did not prerender: the first load should work, and the second should be served from cache. Finally, publish a change in Studio and confirm it appears without a full redeploy. If it does not, the problem has moved to revalidation, not the build.
What to ask a developer
- Which pages are prerendered, and why those?
- How are shared queries cached, and how are they invalidated on publish?
- What happens on first request to a page that was not prerendered?
- Can you show the build log before and after?
If the answers are vague, expect the same problem to return as content grows.
This is work I do. If your deploys have gotten painfully slow, my Performance & Sanity audit, from $800, finds the cause and gives you a prioritised fix list.
Frequently asked questions
01Why is my Next.js build so slow with Sanity?
Usually the build prerenders every document and each page fires its own large GROQ queries, sometimes several. Thousands of pages multiplied by a few heavy queries each is what turns a two minute build into a twenty minute one.
02Should I use generateStaticParams for every Sanity page?
No. Prerender only the pages that get real traffic, such as recent posts and core landing pages, and let the rest generate on first request and be cached. Content that is never visited does not need to be built on every deploy.
03Does a slow build affect SEO?
Not directly, since Google sees the deployed pages, not your build. The indirect effect is real: editors wait to see changes, hotfixes ship slowly, and teams start avoiding deploys, which is when stale or broken content stays live.