iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
✂️

Using next/image with 'next export'

に公開

Introduction

As many of you may already know,
next/image, which I consider to be a major feature of Next.js (lol),
basically cannot be used with next export. [1]

There are projects where you cannot use Vercel but still want to use Next.js.
In such cases, once you've experienced the convenience and excellence of next/image,
creating an image component from scratch is quite exhausting...

With that in mind, I created a component that optimizes images at build time
and provides the functionality of next/image for next export.

If you have the same idea, please use this as a reference,
and I would appreciate it if you could leave a comment with even better ways or operational policies regarding this!

What this article covers

Unfortunately, it is not possible to optimize images at build time using the official next/image,
so this article introduces an unofficial component that has the features of next/image and can be used with next export.

It probably differs from the philosophy of Next.js

I built it myself, but please note that this component differs from the philosophy of Next.js and Vercel.

https://nextjs.org/docs/migrating/from-gatsby#image-component-and-image-optimization

As stated in the official documentation here,
solutions that optimize images at build time
have the disadvantage that build time increases proportionally as the number of images increases.

Therefore, except in cases where you are using so-called static hosting services,
I believe it is basically better to use on-demand image optimization in many cases.

Implementation

I will focus on the key parts for this explanation, so please refer to this repository for the full code.

https://github.com/dc7290/next-export-image

Optimize images using responsive-loader

To optimize images at build time, I decided to use the loader feature since Next.js uses webpack as its bundler.

I adopted responsive-loader because it can generate images of multiple sizes, which is necessary for implementing the "responsive images" feature used in next/image.

https://github.com/dazuaz/responsive-loader

This library can use sharp for image processing, so the build time is relatively fast, which is also a plus.

To modify webpack settings in Next.js, you need to edit next.config.js.

/**
 * @type {import('next/dist/next-server/server/config-shared').NextConfig}
 */
const config = {
  reactStrictMode: true,
  images: {
    disableStaticImages: true,
  },
  webpack: (config) => {
    config.module.rules.push({
      test: /\.(jpe?g|png|webp)$/i,
      use: {
        loader: "responsive-loader",
        options: {
          name: "[path][name].[hash].[width].[ext]",
          outputPath: "static/chunks/images/",
          publicPath: "/_next/static/chunks/images/",
          sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
          placeholder: true,
          adapter: require("responsive-loader/sharp"),
        },
      },
    });

    return config;
  },
};

module.exports = config;

Here is how you set it up.

A brief explanation of the options settings:

  • name: What to name the images
  • outputPath: Where to output the images in the .next directory
  • publicPath: The path used when img tags etc. fetch the images
  • sizes: The sizes of the images to be generated
  • placeholder: Whether to output a placeholder
  • placeholderSize: The size of the image used for the placeholder
  • adapter: You can overwrite the default jimp adapter if you're not using it

With this, when importing jpg, png, or webp, they will be processed by responsive-loader.

Implementation on the Component Side

The overall code, with parts similar to the official Image component omitted, is as follows:

const ExportImage = ({
  src,
  sizes,
  alt,
  priority = false,
  loading,
  lazyBoundary = "200px",
  className,
  placeholder = "empty",
  objectFit,
  objectPosition,
  onLoadingComplete,
  wrapperClassName,
  ...all
}: Props) => {
  //
  // ~~~omitted~~~
  //

  const {
    placeholder: blurDataURL,
    width,
    height,
    srcSet,
    src: outSrc,
  } = require(`~/src/images/${src}`);
  const serSetWebp = require(`~/src/images/${src}?format=webp`).srcSet;

  //
  // ~~~omitted~~~
  //

  let imgAttributes: {
    src: string;
    srcSet?: string;
    serSetWebp?: string;
    sizes?: string;
  } = {
    src: emptyDataURL,
    srcSet: undefined,
    serSetWebp: undefined,
    sizes: undefined,
  };

  if (isVisible) {
    imgAttributes = {
      src: outSrc,
      srcSet,
      serSetWebp,
      sizes,
    };
  }

  return (
    <span className={wrapperClassName} style={wrapperStyle}>
      {sizerStyle && (
        <span style={sizerStyle}>
          {sizerSvg && (
            <img
              style={{
                maxWidth: "100%",
                display: "block",
                margin: 0,
                border: "none",
                padding: 0,
              }}
              alt=""
              aria-hidden={true}
              src={`data:image/svg+xml;base64,${toBase64(sizerSvg)}`}
            />
          )}
        </span>
      )}
      <picture>
        <source srcSet={imgAttributes.serSetWebp} type="image/webp" />
        <img
          {...rest}
          src={imgAttributes.src}
          srcSet={imgAttributes.srcSet}
          sizes={imgAttributes.sizes}
          decoding="async"
          className={className}
          ref={(img) => {
            ref(img);
            handleLoading(img, outSrc, layout, placeholder, onLoadingComplete);
          }}
          style={{ ...imgStyle, ...blurStyle }}
          alt={alt}
        />
      </picture>
      <noscript>
        <img
          {...rest}
          src={outSrc}
          srcSet={srcSet}
          sizes={sizes}
          decoding="async"
          style={imgStyle}
          className={className}
          loading={loading ?? "lazy"}
          alt={alt}
        />
      </noscript>

      {priority ? (
        <Head>
          <link
            key={
              "__nimg-" +
              imgAttributes.src +
              imgAttributes.serSetWebp +
              imgAttributes.sizes
            }
            rel="preload"
            as="image"
            href={imgAttributes.serSetWebp ? undefined : imgAttributes.src}
            imagesrcset={imgAttributes.serSetWebp}
            imagesizes={imgAttributes.sizes}
          ></link>
        </Head>
      ) : null}
    </span>
  );
};

export default ExportImage;

Let's break it down.

First, the component receives the image name via the src prop and imports that image within the component.

const {
  placeholder: blurDataURL,
  width,
  height,
  srcSet,
  src: outSrc,
} = require(`~/src/images/${src}`);
const serSetWebp = require(`~/src/images/${src}?format=webp`).srcSet;

I implemented the component using this data in a way similar to next/image.

A difference from the official version is that it is implemented using the picture tag. Since this component processes images at build time, there is no need to check the user agent to serve different files (WebP vs. original extension); thus, I safely used the picture tag to switch between them.

return (
  <span className={wrapperClassName} style={wrapperStyle}>
    {sizerStyle && (
      <span style={sizerStyle}>
        {sizerSvg && (
          <img
            style={{
              maxWidth: "100%",
              display: "block",
              margin: 0,
              border: "none",
              padding: 0,
            }}
            alt=""
            aria-hidden={true}
            src={`data:image/svg+xml;base64,${toBase64(sizerSvg)}`}
          />
        )}
      </span>
    )}
    <picture>
      <source srcSet={imgAttributes.serSetWebp} type="image/webp" />
      <img
        {...rest}
        src={imgAttributes.src}
        srcSet={imgAttributes.srcSet}
        sizes={imgAttributes.sizes}
        decoding="async"
        className={className}
        ref={(img) => {
          ref(img);
          handleLoading(img, outSrc, layout, placeholder, onLoadingComplete);
        }}
        style={{ ...imgStyle, ...blurStyle }}
        alt={alt}
      />
    </picture>
  </span>
);

Additionally, when the priority prop is true, the image is not lazy-loaded and includes a preloading feature as shown below:

<Head>
  <link
    key={
      "__nimg-" +
      imgAttributes.src +
      imgAttributes.serSetWebp +
      imgAttributes.sizes
    }
    rel="preload"
    as="image"
    href={imgAttributes.serSetWebp ? undefined : imgAttributes.src}
    imagesrcset={imgAttributes.serSetWebp}
    imagesizes={imgAttributes.sizes}
  ></link>
</Head>

Another difference from the official version (and this is a disadvantage) is that because images are processed at build time and the image URLs differ between WebP and the original extension, we must preload only the WebP image.

Conclusion

Thank you for reading this far!
If there are any points that are unclear or incorrect, please feel free to leave a comment or message me on Twitter!

I often feel that the direction and vision Next.js aims for are very ideal, and it is an excellent framework. However, the fact that features are often limited when you want to use export, as in this case, might be a matter of preference for some.

I hope this article helps increase the number of cases where those people can use Next.js, even if only a little.

Future Plans

I am also considering making it possible to provide a component like this as a library in the future.

I have noticed that libraries like the one below still have a significant number of downloads even after updates have stopped, and looking at the issues, there is clearly some demand for image processing at build time.

https://github.com/cyrilwanner/next-optimized-images

Considering that, although the direction is slightly different, I think quite a few people might use something like this component (lol).

I'm not sure if I have the skills to make it happen, but if I do, I'd be happy if you'd give it a try! (lol)

脚注
  1. This is not the case if you use an external image provider. ↩︎

Discussion