Sanity website slow? How to find the cause and what a fix costs

By Nayan Kyada · · 7 min read

Part of The Sanity + Next.js Guide

If your Sanity website is slow, the instinct is to blame the CMS. On the Next.js sites I get asked to look at, Sanity is almost never the bottleneck. The slowness comes from how the frontend asks for content, caches it, and ships it to the browser.

This post is a diagnosis guide. It lists the five causes I see most, how to tell which one you have using tools you already own, and what a fix engagement looks like if you would rather hand it off.

Sanity website slow: start with where the time goes

Before touching code, work out which of three places the delay lives in:

  1. Server time. The HTML takes a long time to arrive. Look at TTFB in Vercel Analytics, Speed Insights, or the Network tab. Anything consistently above roughly 600ms on a content page deserves attention.
  2. Asset weight. The HTML is quick but the hero image or JavaScript is heavy. This shows up as poor LCP with a healthy TTFB.
  3. Browser work. Everything arrives quickly but the page feels sticky or content pops in late. This points at client-side rendering or hydration cost, and shows up as poor INP or a blank-then-filled layout.

That split eliminates half the suspects in ten minutes. If you have not set up field data, my write-up on Core Web Vitals monitoring with Vercel Analytics covers it.

Cause 1: GROQ waterfalls

Symptom: high TTFB on pages that show related content, such as a post with author, category and related posts. How to confirm: log timing around each fetch, or look for several awaits in a row in the page component.

The broken pattern is each component fetching its own data in sequence:

// app/blog/[slug]/page.tsx — sequential fetches
const post = await client.fetch(POST_QUERY, { slug });
const author = await client.fetch(AUTHOR_QUERY, { id: post.author._ref });
const related = await client.fetch(RELATED_QUERY, { cat: post.category._ref });

Three round trips, each waiting on the last. The fix is to let GROQ resolve references in one query:

*[_type == "post" && slug.current == $slug][0]{
  title,
  body,
  "author": author->{name, image},
  "related": *[_type == "post" && category._ref == ^.category._ref && _id != ^._id][0...3]{title, slug}
}

Where queries are genuinely independent, start them together with Promise.all. For the schema side of this problem see structuring Sanity schemas to avoid query waterfalls and avoiding N+1 queries with references in RSC.

Cause 2: uncached fetches

Symptom: every page view is slow by about the same amount, and your Sanity API usage is high for the traffic you have. How to confirm: check whether the route is rendered dynamically on every request. The build output marks routes as static or dynamic, and a content page that should be static showing as dynamic is the giveaway.

Common triggers are reading cookies or headers in a shared layout, leaving draft-mode logic active for all visitors, or fetching with no cache strategy at all. In Next.js 16 the App Router leans toward opt-in caching, so a site that was never given explicit cache directives can quietly re-fetch constantly.

The fix path is to decide per route what can be cached, tag fetches by document type, and revalidate from a Sanity webhook so editors still see changes quickly. I wrote that wiring up in Sanity webhooks with ISR revalidation, and the draft-mode trap in Sanity draft mode without sacrificing edge performance.

Cause 3: oversized or unmanaged images

Symptom: TTFB is fine, LCP is bad, and the LCP element is an image. How to confirm: open the Network tab, filter by images, and look at transferred size and dimensions. A 3000px original served into a 600px slot is typical.

Editors upload what they are given, so the pipeline has to protect you. Pass real width, height and sizes to next/image, use Sanity's image metadata rather than guessing, and mark only the true hero as priority. Details are in killing CLS on Sanity images and fixing LCP on image-heavy pages.

Cause 4: client-side rendering where it is not needed

Symptom: the page shell appears, then content pops in after a spinner. View source shows almost no real text. That is bad for users and for SEO, because crawlers get less to index.

This happens when someone marked a top-level component with use client and fetched Sanity data inside an effect or a client data library. Content pages should fetch in server components and send finished HTML. Reserve client components for the interactive leaves such as a filter or a menu.

Cause 5: bloated JavaScript bundles

Symptom: healthy TTFB and images, but poor INP, long main-thread tasks, and a large JavaScript transfer on mobile. How to confirm: run the Next.js bundle analyzer and look for large client chunks. Portable Text renderers, animation libraries, and full SDK imports in client components are frequent offenders.

A quick check is to see which dependencies appear in client chunks that have no business being there. Two posts cover the fixes in depth: analysing and cutting Next.js chunk bloat and lazy-loading Portable Text. For interaction delay specifically, see INP for React apps.

A quick symptom table

What you seeLikely causeFirst check
High TTFB on related-content pagesGROQ waterfallSequential awaits in page file
High TTFB everywhere, high API usageUncached fetchesStatic vs dynamic in build output
Bad LCP, image is the LCP elementOversized imagesNetwork tab image sizes
Spinner, then contentClient-side renderingView source for real text
Bad INP, big JS transferBundle bloatBundle analyzer

Most slow sites have two or three of these at once, which is why fixing one and seeing little change is common.

What it costs to ignore

Slow pages cost you in three ways: lower conversion on paid and organic traffic, weaker rankings once field data on Core Web Vitals turns poor, and a growing API and hosting bill from uncached requests. I covered the commercial side in what fast marketing pages are actually worth.

What a fix engagement looks like

The shape I use is simple:

  1. Measure. Field data, lab traces on your top templates, and a read-through of the data-fetching and caching code.
  2. Report. A ranked list of causes with the evidence for each, so you can see what matters most and what can wait.
  3. Fix. Highest-impact items first, delivered on a staging URL so you can compare before and after.
  4. Hand off. Notes for your team on what to avoid so the problems do not creep back.

When you talk to any developer about this, ask them which metric they will move, how they will measure it, and whether they will show you the evidence of the cause before quoting the fix. Vague promises to "optimise the site" are a red flag. My questions to ask before hiring a Sanity developer apply here too.

Work I do

This is work I do. The Performance & Sanity audit starts from $800 and gives you the ranked diagnosis described above; details are at services and packages.

Frequently asked questions

Why is my Sanity website slow?

On Next.js sites the cause is rarely Sanity itself. The usual culprits are sequential GROQ fetches, pages that skip caching and hit the API on every request, oversized images, client-side data fetching, and large JavaScript bundles. Each one leaves a different signature in your metrics.

Is the Sanity API slow compared to other headless CMSs?

The Sanity CDN API is generally fast when queries are reasonable and cached. Slow pages usually come from how the frontend calls it: too many round trips, uncached requests, or querying far more data than the page renders.

How long does it take to fix a slow Next.js and Sanity site?

A diagnosis usually takes a few days of measurement and code review. Fixes for caching, image handling and query structure often fit in one to two weeks, while moving a client-rendered site to server components takes longer depending on page count.