How to Create a Custom Image Loader in NextJS
Jasser Mark Arioste
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.
That's basically it!
Full Code and Demo #
The full code is available on GitHub: https://github.com/jmarioste/next-custom-image-loader-tutorial
The demo is available on Vercel: https://next-custom-image-loader-tutorial.vercel.app/
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