ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

Thank you, Next! How to be happy with Next Image's custom loaders

※ こちらはミツモアAdvent Calendar 2021の15日目の記事です。

HI! My name is James and I'm an engineer at MeetsMore. Let's talk about Next.

One of the most useful features that using a framework like Next.js gives you, is image optimization. Recently at MeetsMore, we have been using Next.js to statically generate a whole lot of image heavy pages and I’ve come up against a few issues while employing Next Image along the way. Sometimes the documentation was not enough and required some experimentation and others were issues unique to our situation that I needed to resolve. Here are some tips and ideas that would have made my job a little easier had I known from the start.

What is Next Image?

The next.js framework provides you with an Image component you can use to render images on your site. It optimizes things like lazy loading, responsive source sets, image formats and more so that you can provide 1 source image and don’t have to think much more about it. It’s pretty cool. Using it looks like this:

import myPicture from '../public/me.png'
...
<Image
  src={myPicture}
  alt="Picture of me!"
  width={500}
  height={500}
/>

Different strokes for different hosts

Where you keep your original source images changes everything.

Local Source

If you have all your site’s images in a local directory in your project, you’ll probably have no problems and you can just get on with your life. You don’t even need to explicitly set width and height in the Next Image component. Just import your images and use the import as the source, like in the above example.

Remote Source

You can point Next to images that are hosted somewhere else online, and during the build, Next will download the images, and then process them all locally to be served along side the rest of your build. The only thing here is you have to set an absolute url in the component’s src prop, and explicitly allow Next to access the origin in your next.config.js file. From the docs:

module.exports = {
  images: {
    domains: ['assets.acme.com'],
  },
}

// ... In your component
<Image
  src='https://assets.acme.com/public/me.png'
  alt="Picture of me!"
  width={500}
  height={500}
/>

You can use both local and remote files in your project. As long as you define all the remote origins in your config.

There is another option but it comes with some tradeoffs.

Hosted Images

Finally, if you want to use a service like imgix (like we do at MeetsMore) or Cloudinary, the process is a bit different. Next has some loaders for popular services built in, so you can tell Next to use one in your next.config.js file, then your images won’t get downloaded and processed during build, but the srcset values will be created using the image service’s query parameter settings. This is also pretty cool!

We are using imgix so here’s how that looks. First you set the loader in your config file:

module.exports = {
  images: {
    loader: 'imgix',
    path: 'https://jamthomdev.imgix.net',
  },
}

Then set the Image src as a relative path in the component relative to the url you set in the config:

<Image
  src='/logo.png' // relative to path defined in config
  alt='ミツモア'
  layout='fixed'
  width='139'
  height='28'
/>

And here is the output of the img tag. *Note that Next wraps the image tag in a span not shown in this example.

<img alt="ミツモア"  
srcset=" 
https://jamthomdev.imgix.net/logo.png?auto=format&fit=max&w=256 1x,  
https://jamthomdev.imgix.net/logo.png?auto=format&fit=max&w=384 2x  
"  
src="https://jamthomdev.imgix.net/logo.png?auto=format&fit=max&w=384"  
/>

You can see that Next uses imgix’s query parameters to fill the srcset with the proper image references for different screen sizes / resolutions. The list of URIs in the source set changes depending on the props you set in the Next Image component. We’ll get into that later.

Gotchas

So the thing to be careful with is if you set the loader to use imgix in your next config file, you won’t be able to import images in your components and use them the default way. It's all or nothing. If you are using a hosted service but have some icons or other design elements you want to keep in your repo and use with Next Image it’s not gonna work without a little fiddling. These cases are where we will want to use custom loaders.

Next Image Custom Loaders

One of the things Next Image offers is custom loaders. Here are some example situations where you'd want to use a custom loader:

  • You have a variety of image sources and want to use Next Image for all of them
  • You want to use an image service's custom query parameters creatively to make your images sepia, or circular!

Unfortunately there does not seem to be a way to define a custom loader globally so if you want to use a custom loader you have to apply it directly to every instance of a Next Image component.

First, let's say you want to take advantage of imgix's rendering API so all images become sepia. If you simply set the loader as imgix in your config, you won’t be able to do this since the rendering is predefined by Next. You'll need a custom loader.

First look at how custom loaders are applied in Next. From Next's docs:

A loader is a function returning a URL string for the image, given the following parameters:

  • src
  • width
  • quality
const myLoader = ({ src, width, quality }) => {
  return `https://example.com/${src}?w=${width}&q=${quality || 75}`
}

You pass the loader function to your Image component in the loader prop. A srcset will be output based on the pattern you define in the loader.

If you wanted to expand on the example and make the images sepia, it would look like this:

const mySepiaLoader = ({ src, width, quality }) => {
  return `https://example.com/${src}?w=${width}&q=${quality || 75}&sepia=85`
}
...

<Image
  src='me.png'
  alt="Picture of me!"
  width={500}
  height={500}
  loader={mySepiaLoader}
/>

There is nothing stopping you from creating multiple loaders, and using them creatively. Or combining multiple types of output in one function and changing the return value based on the src, etc.

In fact, this is what we do at MeetsMore. When we transitioned some pages to Next, some of the components props included relative image references and some had absolute references, although all were hosted on imgix. Of course we would prefer to standardize this but in the process of refactoring, we are using a custom loader that checks if the src passed to Next Image is absolute or relative and then returns the correct data. We can use a single custom loader that we can safely apply in many cases and still get the benefits that Next Image offers.

More Gotchas

Notice that a custom loader does not employ a height parameter. So cropping images is not easy to do. Kind of a shame since these kinds of services like imgix can do smart cropping for faces etc. In fact, Next Image doesn't do any cropping for you using the default loader either. If you pass any width and height props that aren't matching the image exactly, the image will be distorted and stretched to fit the dimensions you declare. You can set the objectFit prop to 'cover' and the image won't be distorted, but you will potentially be downloading more image than you need if most of it is out of frame. Kind of a bummer.

Other settings to keep in mind

Hopefully the idea of sourcing images and using custom loaders is a bit clearer now. But there are a few more aspects of Next Image that can be unclear.

Layout

The layout property seems to cause confusion but the docs cover it pretty well. Keep in mind what was mentioned earlier about your images not getting cropped. You may need to use the objectFit prop on images that are used in multiple places where the dimensions vary.

layout="fill" works well for images used in places where the container is a set size, like a hero image. But be aware that images with layout="fill" will use large images by default, big enough to fill the screen since it doesn't know any context and needs to assume that a 100% wide image is possible. If you are worried about serving as small of images as possible, please be careful. If you find yourself using layout="fill" to get a certain design working with smaller containers, you might want to see if other layout methods can work instead.

Sizes and Screens

Finally you can adjust the default sizes that Next Image uses when creating the srcset for your Image components. Here is what a srcset looks like with the defaults:

<img
  srcset="
  https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=640&sepia=80 640w,
  https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=750&sepia=80 750w,
  https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=828&sepia=80 828w, 
  https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=1080&sepia=80 1080w, 
  https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=1200&sepia=80 1200w, 
  https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=1920&sepia=80 1920w,
  https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=2048&sepia=80 2048w, 
  https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=3840&sepia=80 3840w" 
  src="https://jamthomdev.imgix.net/JT060477.JPG?auto=format&fit=max&w=3840&sepia=80"
>

That is a lot of breakpoints. As an example, if you don't care about serving the highest possible resolution images to big screens but you know your source images are large, you might want to edit next.config.js like so:

module.exports = {
  images: {
    deviceSizes: [320, 640, 750, 1080, 1200, 1920],
  },
}

This way the largest images you'll serve up are 1920 px wide (and this example adds an extra small image for if you have a lot of users on tiny screens). If all the largest images on your site are background images that need to load fast, maybe the size is good enough. Next Image tries to do a good job but it still might load big old images if you let it.

Hopefully you will be able to stay in love with Next Image for a bit longer! The more you get to know each other the deeper your relationship can grow.