
Optimizing Sanity Images for Performance
To optimise Sanity images for performance, never serve the original asset URL. Instead, request each image through Sanity's image pipeline with auto=format so browsers get AVIF or WebP, set a width close to the displayed size, and provide a srcSet so phones download smaller files than desktops. Then add explicit dimensions to prevent layout shift, use the lqip metadata as a blurred placeholder, lazy-load images below the fold and preload the main hero image. With @sanity/image-url, most of this is a few chained methods, and in Next.js the Image component from next-sanity/image can handle the srcSet for you.
Images are usually the heaviest part of a page, and they directly affect Core Web Vitals: Largest Contentful Paint (LCP) is often an image, and images without dimensions cause Cumulative Layout Shift (CLS). They also affect your Sanity bill, because image bandwidth counts towards your plan's usage. This guide covers each optimisation in order of impact, with code for Next.js App Router, and explains the trade-offs between next/image and Sanity's own loader.
Start With the Biggest Mistake: Original URLs
The quickest way to slow a site down is to query asset->url and drop it into an <img>:
// Avoid this
<img src={post.mainImage.asset.url} alt="" />
That URL is the untouched upload (the guide to images in Sanity explains how the field and asset relate). An editor's 6,000 pixel photo straight from a camera can be 8 MB or more, and every visitor downloads all of it, even on a phone. Sanity's docs specifically warn that requesting unoptimised images this way can lead to overages in your usage.
The fix is to always add transformation parameters. At minimum, add a width and auto=format:
https://cdn.sanity.io/images/abc123/production/7f3c...-6000x4000.jpg?w=1200&auto=format
That single change often cuts an image's size by 90% or more.
Serve Modern Formats With auto=format
The auto=format parameter tells Sanity's CDN to choose the best format the browser supports, based on its Accept header:
- AVIF: For browsers that support it, usually the smallest output.
- WebP: For browsers that support WebP but not AVIF.
- Original format: For clients that don't advertise support, such as many crawlers.
With the URL builder, it's one method:
import { urlFor } from "@/sanity/lib/image";
const src = urlFor(image).width(1200).auto("format").url();
Two details from Sanity's docs are worth knowing. First, AVIF is generated asynchronously, so the first few requests for a new image may return WebP or the original format until the AVIF version is ready. Second, you can't force AVIF with fm=avif. AVIF is only available through auto=format, so use that rather than format('webp') for browser images.
Use format('jpg') only when the consumer isn't a browser, such as Open Graph images for social platforms or images in HTML emails.
Request the Right Size
A 1200 pixel wide image displayed at 400 pixels wastes two-thirds of its pixels in each direction. Match your requested width to the largest size the image is actually displayed at, accounting for high-density screens.
Use fit=max for Content Images
For images inside articles, combine a width with fit('max'). It resizes large images down but never scales small ones up, which would only make them bigger and blurrier:
urlFor(image).width(1200).fit("max").auto("format").url();
Build a srcSet
Let the browser choose between several widths with srcSet and sizes:
// src/components/sanity-image.tsx
import type { SanityImageSource } from "@sanity/image-url";
import { urlFor } from "@/sanity/lib/image";
const WIDTHS = [320, 640, 960, 1280, 1920];
type Props = {
image: SanityImageSource;
alt: string;
width: number;
height: number;
sizes: string;
priority?: boolean;
};
export function SanityImage({
image,
alt,
width,
height,
sizes,
priority,
}: Props) {
const ratio = height / width;
const src = (w: number) =>
urlFor(image)
.width(w)
.height(Math.round(w * ratio))
.auto("format")
.url();
return (
<img
src={src(960)}
srcSet={WIDTHS.map((w) => `${src(w)} ${w}w`).join(", ")}
sizes={sizes}
width={width}
height={height}
alt={alt}
loading={priority ? "eager" : "lazy"}
fetchPriority={priority ? "high" : "auto"}
decoding="async"
/>
);
}
The sizes attribute is what makes srcSet useful. Without it, the browser assumes the image fills the viewport and may pick a larger file than needed. A typical value for a centred article image is (max-width: 768px) 100vw, 768px.
Keep the list of widths fixed across your site. Identical URLs are cached once on Sanity's CDN and reused, while arbitrary widths create a new transformation for each variation. Sanity's own docs recommend reusing transformations for this reason.
Fixed-Size Images and dpr
For images that are always the same size, such as avatars and logos, use dpr instead of a width list:
<img
src={urlFor(author.photo).width(64).height(64).url()}
srcSet={[1, 2, 3]
.map(
(d) =>
`${urlFor(author.photo).width(64).height(64).dpr(d).auto("format").url()} ${d}x`,
)
.join(", ")}
width={64}
height={64}
alt={author.name}
/>
Choose Your Next.js Approach
In Next.js you have three ways to render Sanity images, and they don't perform the same:
| Approach | Who resizes and encodes | Config needed | Notes |
|---|---|---|---|
next/image with default loader | Next.js image optimisation | images.remotePatterns | Images are processed twice, and your host may charge for optimisation |
next/image with imageLoader | Sanity's CDN | None for Sanity | Loader must be in a client file if you wrap it |
Image from next-sanity/image | Sanity's CDN | None | Sets auto=format and fit for you; currently alpha |
With the default loader, Next.js downloads the image from Sanity, re-encodes it and serves it from your own server or host. That duplicates work Sanity's CDN already does, and on some platforms image optimisation is metered. Using Sanity's CDN directly is usually the better choice.
Here's the next-sanity/image version:
// src/components/post-hero.tsx
import { Image } from "next-sanity/image";
import { urlFor } from "@/sanity/lib/image";
type PostHeroProps = {
image: {
asset: { _ref: string };
alt?: string;
lqip?: string;
};
};
export function PostHero({ image }: PostHeroProps) {
return (
<Image
src={urlFor(image).width(1600).height(900).url()}
alt={image.alt ?? ""}
width={1600}
height={900}
sizes="(max-width: 1280px) 100vw, 1280px"
placeholder={image.lqip ? "blur" : "empty"}
blurDataURL={image.lqip}
preload
/>
);
}
The component's loader rewrites the URL for each srcSet width, keeps the aspect ratio by recalculating h, and adds auto=format. It requires a valid Sanity CDN URL as src, which is why you build it with urlFor first. If you haven't set up the builder, see using Sanity's image URL builder.
Prevent Layout Shift
Images without dimensions cause content to jump as they load. Always give the browser the aspect ratio, either with width and height attributes or with the CSS aspect-ratio property.
For content images where you don't know the size in advance, read it from the asset. You can dereference the metadata in GROQ:
*[_type == "post" && slug.current == $slug][0]{
mainImage{
...,
"dimensions": asset->metadata.dimensions,
"lqip": asset->metadata.lqip
}
}
Or read dimensions straight from the asset ID without a dereference, using @sanity/asset-utils:
import { getImageDimensions } from "@sanity/asset-utils";
const { width, height, aspectRatio } = getImageDimensions(image);
Asset IDs include the original dimensions, so this works on any image field without extra query cost. It's particularly handy inside Portable Text, where image blocks usually only contain a reference.
Add Blurred Placeholders
Every image asset gets an lqip value by default: a tiny base64 data URL around 20 pixels wide. It's small enough to inline in your HTML and gives readers an immediate impression of the image while the real one loads.
With next/image or next-sanity/image, pass it as blurDataURL with placeholder="blur". With a plain <img>, use it as a background:
<div
style={{
backgroundImage: `url(${image.lqip})`,
backgroundSize: "cover",
aspectRatio: `${width} / ${height}`,
}}
>
<SanityImage
image={image}
alt={image.alt ?? ""}
width={width}
height={height}
sizes="100vw"
/>
</div>
Sanity also generates blurHash and thumbHash by default. They're more compact and often look smoother, but you need a decoding library on the client. For most sites, lqip is the simplest option because it needs no JavaScript.
For a richer effect, the palette metadata gives you a dominant colour you can use as a solid background while the image loads:
"bg": asset->metadata.palette.dominant.background
Lazy-Load Below the Fold, Preload Above It
Browsers support native lazy loading with loading="lazy", and next/image uses it by default. Use it for everything except images visible on first load.
Your LCP image, usually the hero, needs the opposite treatment. It should load as early as possible. In Next.js 16, use the preload prop on next/image or next-sanity/image. With a plain <img>, set loading="eager" and fetchPriority="high".
Only preload one or two images per page. Preloading everything defeats the purpose by competing for bandwidth.
Control Quality
Sanity's default compression quality is 75 for JPG and WebP. That's a sensible balance for photos. You can lower it for large decorative backgrounds or raise it for product photography where detail matters:
// Decorative background
urlFor(image).width(1920).quality(60).auto("format").url();
// Product detail
urlFor(image).width(1600).quality(85).auto("format").url();
Test changes visually. Below about 60, artefacts start to appear in gradients and skin tones.
Handle Special Cases
- Animated GIFs: Large animated GIFs are expensive. Sanity's CDN can output animated WebP, which is usually much smaller, and
frame(1)returns just the first frame for a static thumbnail. Animated transformations over 256 megapixels across all frames return a static image instead. - SVGs: Vector images are already small and scale perfectly. Sanity can rasterise them, but for logos and icons, serving the original SVG is often the better choice.
- Very large outputs: According to Sanity's asset CDN docs, image responses over 10 MB aren't cached on the CDN and are served from the backend. Requesting sensible sizes keeps you well under that.
- Video: Don't use images or files for long videos. Use a streaming service such as the official Mux integration, or Media Library video if you're on an eligible Enterprise plan.
Stop Oversized Uploads at the Source
Performance starts in the Studio. A custom validation rule can warn editors when they upload something far larger than you need, using the dimensions in the asset ID:
// schemaTypes/objects/optimisedImage.ts
import { defineField, defineType } from "sanity";
import { getImageDimensions } from "@sanity/asset-utils";
export const optimisedImage = defineType({
name: "optimisedImage",
title: "Image",
type: "image",
options: {
hotspot: true,
accept: "image/jpeg, image/png, image/webp, image/avif",
},
fields: [defineField({ name: "alt", type: "string" })],
validation: (rule) =>
rule
.custom((value) => {
if (!value?.asset?._ref) return true;
const { width } = getImageDimensions(value.asset._ref);
if (width < 1200)
return "Image is narrower than 1200px and may look blurry on large screens.";
return true;
})
.warning(),
});
Warnings don't block publishing, so editors can still proceed if they need to. Because Sanity resizes on demand, a very large upload doesn't slow your pages as long as you always request a sized URL. Minimum sizes are the more important check. For more about rules like this, see validation rules for Sanity fields.
Also consider the metadata option. If you never use blurhash, thumbhash or palette, you can limit extraction to what you need, such as metadata: ['lqip']. Don't turn on exif or location unless you need them, since they can expose private information.
Measure the Results
After making changes, check them with real tools:
- Open Chrome DevTools, go to the Network tab, filter by Img and confirm that responses are
aviforwebpand sized close to the display size. - Run Lighthouse or PageSpeed Insights and look at "Properly size images", "Serve images in next-gen formats" and the LCP element.
- Watch the Usage page in your Sanity project settings after deploying. Bandwidth should drop noticeably if you were serving originals. The Sanity pricing guide explains how bandwidth counts towards your plan.
FAQ: Optimising Sanity Images
Yes, through auto=format. Browsers that advertise AVIF support in their Accept header receive AVIF, others receive WebP or the original format. You can't request AVIF directly with fm=avif, and the first few requests for a new image may return WebP while AVIF is generated.
If you use next/image with its default loader, Next.js re-processes images Sanity has already optimised. The next-sanity/image component, or next/image with the imageLoader from next-sanity/image, sends requests straight to Sanity's CDN instead, which avoids duplicate work.
lqip stands for low-quality image placeholder. It's a tiny base64-encoded version of the image, generated on upload by default, that you can inline as a blurred placeholder while the full image loads.
Always set width and height or a CSS aspect ratio. You can get the original dimensions from asset->metadata.dimensions in GROQ, or from the asset ID with getImageDimensions from @sanity/asset-utils.
Not if you always request sized URLs, because Sanity resizes on demand and caches the result. They only slow pages down if you serve the original asset URL directly, which also increases your bandwidth usage.
Sanity's default of 75 for JPG and WebP suits most photos. Lower it to around 60 for large decorative images, or raise it to 80 to 90 for product photography, and check the results visually.
Conclusion
Optimising Sanity images is mostly about letting the image pipeline do its job. Request every image with a width and auto=format, use fit('max') for content images, provide a srcSet with an accurate sizes value and keep your widths consistent so the CDN cache works for you. Add dimensions to stop layout shift, use lqip for instant placeholders and preload only the hero image.
In Next.js, prefer a loader that talks to Sanity's CDN directly rather than re-optimising images on your own server. Combine that with sensible upload rules in the Studio, measure with DevTools and Lighthouse, and your pages will load faster while your bandwidth usage goes down.


