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="/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:

"A West Texas landscape at sunset."

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&amp;w=384&amp;q=75 1x, /_next/image?url=%2Flandscapeimage.png&amp;w=640&amp;q=75 2x"
  src="/_next/image?url=%2Flandscapeimage.png&amp;w=640&amp;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:

"A West Texas landscape at sunset."

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.js
export 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.js
module.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}
    />
  );
}