Custom Image Loaders in Next.js
As I've been overhauling https://joeyreyes.rocks, I've been taking the opportunity to learn more about Next.js features. For a long time I avoided using Custom Image Loaders, and I'm not really sure why that is. It turns out this feature isn't as scary or complicated as the name might make it sound. This blog post details what a Custom Image Loader is and when you might use it. This is a guide for using Custom Image Loaders in the App Router (although the concept translates pretty easily to the Pages Router).
next/image
Let's start with Next.js's built-in image optimization feature, next/image
. Taken straight from the docs, you import next/image
and use it like so:
import Image from "next/image";
export default function Test() { return ( <Image src="https://dliq7zg4033c0.cloudfront.net/blog/custom-image-loaders-in-nextjs/landscapeimage.png" alt="A West Texas landscape at sunset." width="300" height="200" /> );}
This loads an image to a Next.js page that looks something like the following:
In this case, landscapeimage.png
can be found right in the public
folder in the root directory of the Next.js app.
next/image
handles things like image compression, serving images in modern formats (e.g., webp), supports lazy loading, and so on. This is all an effort to make user experiences better on websites and improve core web vitals by minimizing Cumulative Layout Shift, all while making it easier for devs to ship images without digging into something like the <picture>
element, which can get complicated pretty quickly.
The component above returns something like the following:
<img alt="A West Texas landscape at sunset." loading="lazy" width="300" height="200" decoding="async" data-nimg="1" style="color:transparent" srcset=" /_next/image?url=%2Flandscapeimage.png&w=384&q=75 1x, /_next/image?url=%2Flandscapeimage.png&w=640&q=75 2x " src="/_next/image?url=%2Flandscapeimage.png&w=640&q=75"/>
The problem with next/image
is that it's not always the best solution for every website or tech stack. When you run Next.js's build step, it runs the image optimization step, which requires some computing power and memory to process those images. This isn't a big deal when you're doing local development on your own computer, but a lot of cloud hosting and CI tools will charge you for that processing effort. Many of them also don't support next/image
out of the box.
Furthermore, a lot of CMS and DAM tools already offer their own image optimization and sizing features. These usually take the shape of query parameters that you append to the url of the image src
. Say, for example, you use Contentful as your CMS, and you are uploading and using images from the Contentful Media Library. Out of the box, Contentful lets you specify things like the width, height, file format, and more just by appending query params. See Contentful's Images API docs for more details. Here's that same image, loaded from Contentful:
It's pretty small here because I've specified that I want to load that image with a 300px
width and a 200px
height:
https://images.ctfassets.net/ygdvy6c648m3/4rDSciBVnOl4UMbUAACDZo/96ac031239f7417177487d4877b341e7/landscapeimage.png?w=300&h=200
The key things to note are the query params at the end of the url: ?w=300&h=200
. It would be somewhat duplicative work to use next/image
with both of these things:
import Image from "next/image";
export default function Page() { return ( <Image src="//images.ctfassets.net/ygdvy6c648m3/4rDSciBVnOl4UMbUAACDZo/96ac031239f7417177487d4877b341e7/landscapeimage.png?w=300&h=200" alt="A West Texas landscape at sunset." width="300" height="200" /> );}
This example also won't work because I haven't configured the hostname in next.config.js
. I could modify that file and do so, but that would be a bit of a pain.
Furthermore, unless I deploy to Vercel, it's not guaranteed that my hosting or CI provider will support next/image
. So what if I was able to tell next/image
that I want to use optimization from Contentful instead of next/image
? That's where a Custom Image Loader comes in.
Custom Image Loader
In Next.js, a Custom Image Loader is simply a function that tells next/image
what kind of src
to return:
// src/imageLoader.jsexport default function contentfulLoader({ src, quality, height, width }) { const url = new URL(`https:${src}`); url.searchParams.set("fm", "webp"); url.searchParams.set("q", (quality || 75).toString()); url.searchParams.set("h", (height || 200).toString()); url.searchParams.set("w", (width || 300).toString()); return url.href;}
Here we've created an example imageLoader.js
file and saved it to the src
directory. It accepts src
, quality
, height
, and width
arguments, which would be parameters such as source url, quality value, height, and width. It returns a URL with each of these query parameters and the specified values appended.
We can use this in one of two ways. First, we can pass it directly to a single instance of next/image
in order to use the custom loader just for that specific image:
"use client";
import Image from "next/image";import imageLoader from "../src/imageLoader";
export default function Page() { return ( <Image loader={imageLoader} src="//images.ctfassets.net/ygdvy6c648m3/4rDSciBVnOl4UMbUAACDZo/96ac031239f7417177487d4877b341e7/landscapeimage.png" alt="A West Texas landscape at sunset." width={300} height={200} /> );}
You can also modify your next.config.js
file to specify a custom loader to use on all images:
// next.config.jsmodule.exports = { // ... other config stuff images: { loader: "custom", loaderFile: "./src/imageLoader.js", },};
In this way you don't have to opt into the custom loader for every single image:
import Image from "next/image";
export default function Page() { return ( <Image src="//images.ctfassets.net/ygdvy6c648m3/4rDSciBVnOl4UMbUAACDZo/96ac031239f7417177487d4877b341e7/landscapeimage.png" alt="A West Texas landscape at sunset." width={300} height={200} /> );}