ReactHustle

How to Create a Custom Image Loader in NextJS

Jasser Mark Arioste

Jasser Mark Arioste

How to Create a Custom Image Loader in NextJS

Hello, hustler! In this tutorial, you'll learn how to set up a custom image loader in NextJS to avoid or reduce image optimization limitations in Vercel.

Introduction #

The NextJS <Image/> component is a handy component when it comes to image optimizations. However, if you deployed your web application in Vercel you might want to create a custom image loader. If you're on the free tier, you only have 1000 source image optimizations which can be easily used up and get your account banned and your website unreachable. This was one of the pending problems in this blog, the more traffic I get the more source image optimizations I use.

By using a custom image loader, you avoid the usage costs in Vercel and that's what we'll do in this tutorial. This is guide is also useful if you're not using Cloudinary or other image providers. 

Step 1 - Creating a Custom Image Component #

First, let's create a custom image component that uses a custom loader.

// components/MyImage.tsx
import { ImageProps } from "next/image";
import React from "react";
import Image from "next/image";
const MyImage = (props: ImageProps) => {
  return (
    <Image
      {...props}
      alt={props.alt}
      loader={({ src, width: w, quality }) => {
        const q = quality || 75;
        return `/api/custom-loader?url=${encodeURIComponent(src)}?w=${w}&q=${q}`;
      }}
    />
  );
};
export default MyImage;

1234567891011121314151617

We just create a wrapper component that has a loader property. It uses the /api/custom-loader endpoint to optimize the images rather than relying on the default /_next/image. Note that opting out of the default loader will remove the optimizations that Vercel provides, such as the CDN, but if you don't mind this, then carry on!

Step 2 - Implementing the Custom Loader API #

Next, we'll implement the custom loader API to transform a large image to the appropriate size and webp format. Here we're implementing the custom loader in the same app but it can also be on another backend server.

First, let's install sharp and axios npm packages. The sharp library is an image transformation library. NextJS also uses this internally. We also use axios since I find it easier when requesting the src image using axios.get() method.

yarn add sharp axios && yarn add -D @types/sharp

Next, let's create the file pages/api/custom-loader.ts and copy the code below:

// pages/api/custom-loader.ts
import axios, { AxiosError } from "axios";
import sharp from "sharp";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler( req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "GET") {
    try {
      // 👇 get the image src,width, andq quality from query params
      const url = decodeURI(req.query.url as string);
      if (!url) {
        res.status(400).send("400 Bad request. url is missing");
        res.end();
        return;
      }
      const width = (req.query.w as string) ?? "384"; //default width
      const quality = (req.query.q as string) ?? "75"; //default quality
      // 👇 get the image data using axios
      const response = await axios.get(decodeURI(url), {
        responseType: "arraybuffer",
      });
      // 👇 use sharp to resize the image based on the parameters
      const optimized = await sharp(response.data)
        .resize({
          withoutEnlargement: true,
          width: parseInt(width),
        })
        .webp({ quality: parseInt(quality) }) //transform to webp format
        .toBuffer();
      // 👇set public cache to 1 year
      res.setHeader(
        "Cache-Control",
        "public, max-age=31536000, must-revalidate"
      );
      // 👇set content type to webp.
      res.setHeader("content-type", "image/webp");
      // 👇send buffered image
      res.status(200).send(optimized);
      res.end();
    } catch (e) {
      if (e instanceof AxiosError) {
        res.status(500);
        res.end();
      }
    }
  } else {
    res.status(405).send("Method not allowed");
  }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748

Explanation:

Lines 3-4: import the necessary packages.

Lines 9-16: We get the necessary data from the query parameters, which are the source URL(src), width (w), and quality (q).

Lines 18-20: We get the image data from the using axios.

Lines 22-28: Use sharp to transform the image. If you need, more transformations you can check the sharp docs.

Lines 30-33: Set the cache-control header for public caching. Caching the response will also reduce usage costs in Vercel.

Step 3 - Usage and Testing #

Next, let's see if our custom loader works by using our custom MyImage component along with the default Image component.

// pages/index.tsx
import type { NextPage } from "next";
import Image from "next/image";
import MyImage from "../components/MyImage";
const Home: NextPage = () => {
  const src =
    "https://cdn.pixabay.com/photo/2022/12/12/17/05/elephants-7651446_960_720.jpg";
  return (
    <div>
      <main>
        <p>With custom loader</p>
        <div style={{ position: "relative", width: "50%", paddingTop: "100%" }}>
          <MyImage
            src={src}
            fill
            style={{ objectFit: "cover" }}
            alt="elephant image"
          />
        </div>
        <p>Default image</p>
        <div style={{ position: "relative", width: "50%", paddingTop: "100%" }}>
          <Image
            src={src}
            fill
            style={{ objectFit: "cover" }}
            alt="elephant image"
          />
        </div>
      </main>
    </div>
  );
};
export default Home;
123456789101112131415161718192021222324252627282930313233

It uses the same image source, and now let's compare the network responses.

Next JS Custom Loader results

That's basically it!

Full Code and Demo #

Conclusion #

We learned how to use and set up a custom image loader in NextJS using the sharp and axios packages. Using a custom loader may not be the most efficient way to serve the images but it serves as a way to reduce the image optimization usage in Vercel.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Resources #

Credits: Image by Ilona Ilyés from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle