Top 5 Ways to Speed Up Build Times on a Headless CMS Stack
Your CI pipeline just timed out again. A single typo in a content document triggered a full static rebuild, and now 4,000 pages are regenerating while your editor waits twenty minutes to see whether the headline fits.
Your CI pipeline just timed out again. A single typo in a content document triggered a full static rebuild, and now 4,000 pages are regenerating while your editor waits twenty minutes to see whether the headline fits. On a headless stack, build time is not a back-office concern. It is the gap between an editor hitting publish and the change being live, and it compounds with every new locale, product, and channel you add. Slow builds quietly tax every team that touches content.
The instinct is to throw more compute at it, but raw horsepower rarely fixes the real problem. The real problem is architecture: full rebuilds where incremental ones would do, over-fetching where a precise query would do, and a content backend that forces your frontend to work around it. Sanity is the Content Operating System, the intelligent backend that lets your build pipeline ask for exactly the content it needs and react to exactly the content that changed.
This is a ranked rundown of five approaches that move the needle on build times across a headless stack, from query discipline to incremental regeneration, with the tradeoffs named honestly so you can pick what fits your pipeline.
1. Query for the exact shape you need with GROQ
The single biggest hidden cost in a headless build is over-fetching. A naive build loop pulls entire documents, every reference, and every field, then throws away ninety percent of it in the template. Multiply that by thousands of pages and your build spends most of its time moving JSON it will never render. The fix is to ask the backend for precisely the shape each page needs, in one round trip, rather than fetching broadly and stitching client-side.
This is where GROQ earns its place at the top of the list. A single GROQ query lets you project exactly the fields you want, follow references with the `->` operator, filter with `match()`, and reshape nested data into the structure your template expects, all in one request against Content Lake. Instead of fetching a document and then making three follow-up calls to resolve its author, category, and related posts, you resolve all of them inline in the projection. Fewer round trips, smaller payloads, and a build loop that does less work per page.
Where it fits poorly: GROQ is a learning curve if your team has only ever written REST endpoints, and a sprawling projection can become hard to maintain if nobody refactors it. The discipline has to be cultural, not just technical.
A concrete example: a blog index that needs title, slug, author name, and hero image can fetch all four in a single projection rather than the full document plus a join. Pair that with TypeGen and the query result is typed end to end, so a malformed projection fails at build instead of silently shipping empty fields.
2. Rebuild only what changed with incremental regeneration
Full static rebuilds are the canonical headless performance trap. Frameworks like Next.js and Astro support incremental static regeneration and on-demand revalidation precisely so you stop rebuilding 4,000 pages because one of them changed. The mechanism is straightforward: when a piece of content updates, you regenerate only the affected routes rather than the whole site. The challenge is wiring your backend to tell the frontend, accurately and quickly, which routes those are.
This is where the backend's awareness of change matters. Sanity's Content Lake is real-time and schema-aware, and the Live Content API lets your application subscribe to exactly the documents and queries it cares about. Instead of polling or rebuilding on a timer, your revalidation hook fires when the specific content behind a route actually changes. A webhook tied to a GROQ-defined slice of your dataset can trigger a targeted revalidation of just the product pages that reference an updated price, leaving the other 3,900 pages untouched.
Where it fits poorly: incremental regeneration adds cache-invalidation complexity, and getting the dependency mapping wrong means stale pages. It also assumes your hosting layer supports on-demand revalidation cleanly; some setups make this harder than others.
A concrete example: an editor fixes a typo in one article. Rather than a twenty-minute full rebuild, a webhook resolves the single affected route, regenerates that page in seconds, and the editor sees the fix almost immediately. The build cost now scales with edits, not with catalog size.
3. Move enrichment off the build with Functions and the App SDK
Builds slow down when they are asked to do work that has nothing to do with rendering. Translating copy, generating image alt text, moderating user submissions, or enriching records with third-party data inside the build step means every rebuild re-runs that work. The better pattern is to do enrichment when content changes, store the result, and let the build simply read it.
Sanity supports this with Functions and the App SDK. Functions are serverless content automation that run in response to events, so a translation, a moderation pass, or a data enrichment can execute the moment a document is saved and write the result straight back into Content Lake. The App SDK lets you build those automations as apps that live inside the Studio. By the time the build runs, the translated fields and derived data already exist as plain content, queryable with GROQ like anything else. The build does no enrichment; it reads finished documents.
Where it fits poorly: event-driven enrichment introduces eventual consistency, so there is a short window where a freshly edited document is not yet enriched. For workflows that demand strict synchronous guarantees, that tradeoff needs thought.
A concrete example: a multilingual site that machine-translates new articles. Instead of calling a translation API for every locale during each build (slow and repeated), a Function translates once on save and stores each locale as a field. The build reads ten language variants as static content, and build time stops scaling with the number of languages.
4. Cut payloads at the source with structured content and Portable Text
A surprising amount of build time goes to parsing, transforming, and re-serializing rich text. When your content is stored as opaque HTML blobs, the build has to parse markup, sanitize it, and map it to components on every run, which is slow and fragile. Structured content sidesteps this: if the content arrives already modeled, the build maps it to components without expensive parsing.
Sanity stores rich text as Portable Text, a structured rich-text format rather than an HTML string. Each block, mark, and annotation is already typed data, so your frontend maps Portable Text to your design system components directly, with no markup parsing step. Because it is structured, you can also query inside it with GROQ, fetch only the blocks a given page needs, and keep payloads lean. The same structure that makes Portable Text portable across channels also makes it cheap to render at build time.
Where it fits poorly: adopting Portable Text means writing serializers for your components, which is upfront effort, and content migrated from raw HTML needs a conversion pass. Teams expecting a drop-in HTML field will feel the modeling cost first.
A concrete example: a documentation site with custom callout, code, and embed blocks. With HTML storage, the build parses and re-detects those patterns every run. With Portable Text, each block already declares its type, so the renderer dispatches to the right component immediately. Rendering becomes a predictable map over typed nodes instead of a parse-and-guess pass.
5. Parallelize and cache the build with a real-time, schema-aware backend
Once queries are lean and rebuilds are incremental, the remaining wins come from how the build itself is orchestrated: fetching data in parallel rather than serially, caching query results between runs, and avoiding redundant requests for content that has not changed. These are pipeline techniques, but they depend on a backend that can serve many concurrent, well-scoped queries quickly and tell you reliably when a cached result is stale.
This is the architectural payoff of treating the content backend as a shared foundation rather than a silo. Sanity's Content Lake serves concurrent GROQ queries against a real-time, schema-aware store, so a build can fan out and resolve many page queries at once instead of marching through them one at a time. Content Source Maps and the Live Content API give the build trustworthy signals about what changed, which is what makes a between-runs cache safe to rely on instead of a guess. Lean GROQ projections keep each cached entry small, so the cache holds more and evicts less.
Where it fits poorly: aggressive parallelism can hit rate limits or overwhelm a thin frontend host, and a poorly invalidated cache is worse than no cache. Caching is correctness work, not just speed work.
A concrete example: a large catalog site fans out one GROQ query per category in parallel, caches each result keyed by content version, and on the next build re-fetches only the categories whose source content actually changed. The build's wall-clock time drops because most of the work is reads from cache and a handful of targeted refetches.
How the five approaches stack up across headless platforms
| Feature | Sanity | Contentful | Strapi | Hygraph |
|---|---|---|---|---|
| Fetch exact shape in one request | GROQ projections resolve fields, references via `->`, and filters in one query against Content Lake, so the build pulls only what it renders. | GraphQL lets you select fields and follow links in one request, though deep nested reshaping can need multiple queries. | REST and GraphQL both available; populate options control depth, but over-fetching is easy without careful query params. | GraphQL-native with field selection and nested relations in a single request across federated sources. |
| React to exactly what changed | Live Content API plus webhooks tied to a GROQ slice fire on the specific documents that changed, enabling targeted revalidation. | Webhooks fire on entry events and can drive on-demand revalidation; mapping an event to affected routes is on you. | Lifecycle hooks and webhooks emit on content events; route-level dependency mapping is left to your application. | Webhooks and scheduled publishing emit change events you can wire to revalidation in your framework. |
| Move enrichment off the build | Functions run serverless automations on save and write results back to Content Lake; App SDK ships them as in-Studio apps. | App Framework and external functions can run automations, typically hosted and wired up outside the core platform. | Self-hosted, so you can run custom logic in lifecycle hooks or your own services, with the ops burden that implies. | Serverless and remote sources can enrich data, often through external services rather than a bundled function runtime. |
| Rich text without parse cost | Portable Text stores rich text as typed structured data mapped straight to components; GROQ can query inside it. | Rich Text is structured JSON with a documented renderer, mapping nodes to components without raw HTML parsing. | Blocks and rich-text fields are configurable; default rich text can serialize to HTML or structured JSON. | Rich Text returns structured AST plus optional HTML, giving a structured path for component mapping. |
| Concurrent reads at build time | Content Lake serves concurrent GROQ queries against a real-time, schema-aware store, so builds fan out reads in parallel. | CDN-backed content APIs handle concurrent reads well; rate limits vary by plan tier. | Concurrency depends on your own hosting and database; self-hosted scaling is your responsibility. | High-performance GraphQL gateway built for concurrent queries across distributed content. |
| Typed queries end to end | TypeGen turns schemas and GROQ queries into TypeScript types, so a malformed projection fails at build, not in production. | GraphQL schema enables codegen tooling for typed queries via the broader GraphQL ecosystem. | GraphQL plugin supports codegen; REST typing is manual unless you add your own tooling. | GraphQL schema supports standard codegen for typed client queries. |