Presenting images that look good and load fast is essential to provide a good user experience. Fortunately in 2023 we have a lot of services and APIs that help achieve that goal, but understanding how to use them can be difficult.
In this post I'll explain how to render images in a Next.js + Sanity website in hope that the lessons learned can be transferred to similar technologies.
The Toolbox
Presenting images requires a method to serve them and one render them. For the first bit we can use the Sanity Asset CDN, and for the other one there is the Next.js image component.
Sanity Asset CDN
On top of storage and delivery, sanity provides a pipeline to transform the images upon request using query parameters.
We can use the w
and h
parameters to resize the image to the dimensions we need. This flexibility allows us to upload a high definition image and have the frontend request a properly sized version using the srcset attribute.
<img
alt="Logo"
src={IMG_URL}
srcset={`
${IMG_URL}?=w=72 72w,
${IMG_URL}?=w=144 144w
`}
width={72}
height={72}
/>
In this example we render a logo icon using a 72x72 image from Sanity. The source set includes a url for the size we need plus one with doubled resolution. The browser will use the second one on devices with high resolution screens.
Another case where this API is useful is with background images. Let's imagine a hero image that covers the entire viewport.
Here are the styles:
section {
position: relative;
width: 100vw;
height: 100vh;
display: grid;
place-content: center;
}
section > img {
position: absolute;
inset: 0;
object-fit: cover;
margin: 0 auto;
}
Since the image will adapt to the dimensions of the viewport, we need to include a lot of sources, starting from 400px
. Note that the same considerations about high quality displays apply here as well, therefore if we stop at 2000px
, the image won't look sharp on high resolutions screens from 1000px
wide onwards.
<section>
<h2>Welcome!</h2>
<img alt=""
src={IMG_URL}
srcSet={`
${IMG_URL}?w=400 400w,
${IMG_URL}?w=600 600w,
${IMG_URL}?w=800 800w,
${IMG_URL}?w=1000 1000w,
${IMG_URL}?w=1200 1200w,
${IMG_URL}?w=1400 1400w,
${IMG_URL}?w=1600 1600w,
${IMG_URL}?w=1800 1800w,
${IMG_URL}?w=2000 2000w
`}
/>
</section>
As last example, let's consider another type of hero, where the image and the heading are stacked. On mobile the image will be 100% of the screen, but on desktop it will cover just half of it.
section {
position: relative;
width: 100vw;
height: 100vh;
display: grid;
}
section > img {
width: 100%;
height: 100%;
object-fit: cover;
}
@media (min-width: 1024px) {
section {
grid-template-columns: 1fr 1fr;
}
section > h2 {
width: 50%;
height: 100%;
display: grid;
place-content: center;
}
}
To render the image with the right dimensions we can keep the source set as it is, and add the sizes
attribute.
<img alt=""
src={IMG_URL}
srcSet={`
${IMG_URL}?w=400 400w,
${IMG_URL}?w=600 600w,
${IMG_URL}?w=800 800w,
${IMG_URL}?w=1000 1000w,
${IMG_URL}?w=1200 1200w,
${IMG_URL}?w=1400 1400w,
${IMG_URL}?w=1600 1600w,
${IMG_URL}?w=1800 1800w,
${IMG_URL}?w=2000 2000w
`}
sizes="(min-width: 1024px) 50vw, 100vw"
/>
The browser will consider 100vw
to be the image size until the media query matches, then it will consider it to be 50vw
.
Next.js Image component
The Next.js image component is a wrapper around the HTML img tag that provides useful features like:
- Auto generating srcsets
- Static image generation
- Lazy loading
- Blurred placeholders
- Sensible defaults
- Automatic preload
To render an image, simply import the component from next/image
and pass props to it.
import Image from "next/image";
// Image with static dimensions
<Image src={IMG_URL} alt="" width={72} height={72} />
// Responsive image
<section>
<h1>Welcome!</h2>
<Image src={IMG_URL} alt="" fill />
</section>
The resulting html will be similar to the examples from before with one notable exception:
Instead of using the Sanity CDN, Next.js will download and cache the images internally and serve them from /_next/images
.
This is fine if you are deploying on Vercel because they will serve the generated images from a CDN, but if you are deploying on a node server located in a single data center, images will load slowly.
If images take too long to load you can fix it by disabling static image generation via the unoptimized
prop, but then you would have to write the srcset
manually.
In this case a better option would be to use the loader
prop to tell the image component that it should fetch the images from the Sanity CDN instead of the Next.js image cache.
Using the Image component with the Sanity CDN
Start by installing the image url package.
npm i @sanity/image-url
# yarn add @sanity/image-url
# pnpm add @sanity/image-url
We will use this library top facilitate the creation of different image urls that take advantage of the image transformation functionality offered by Sanity.
Before using the library we need to feed our sanity client instance to it.
// lib/sanity.ts
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
export const client = createClient({
projectId: 'your-project-id',
dataset: 'your-dataset-name',
useCdn: true, // not important
apiVersion: '2023-06-12'
})
const builder = imageUrlBuilder(client);
export function urlForImage(source: SanityImageSource) {
return builder.image(source)
}
Now, let's create a React component to render the section from before using next/image
in combination with the Sanity CDN:
"use client";
import { urlForImage } from "../lib/sanity";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import Image from "next/image";
type Props = {
image: SanityImageSource;
};
export default function MySection({ image }: Props) {
return (
<section>
<h2>Welcome!</h2>
<Image
src="Doesn't matter"
alt=""
fill
loader={({ width, quality = 100 }) =>
urlForImage(image).width(width).quality(quality).url()
}
/>
</section>
);
}
This implementation gives us the benefits of the image components with the convenience and lower cost of the Sanity CDN.
We can elaborate our approach further by creating a reusable SanityImage component:
"use client";
import { urlForImage } from "@/sanity/lib/image";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import Image, { ImageProps } from "next/image";
type Props = Omit<ImageProps, "src"> & {
src: SanityImageSource;
};
export default function SanityImage({ src, alt, ...props }: Props) {
return (
<Image
src="Doesn't matter"
alt={alt}
loader={({ width, quality = 100 }) =>
urlForImage(src).width(width).quality(quality).url()
}
{...props}
/>
);
}
Top comments (0)