🐈

Next.jsの画像最適化をnext/imageを使用せずに活用する方法

2023/09/20に公開

はじめに

v13.4.8 からunstable_getImgProps というメソッドが next/image から提供されるようになりました。
これは next/image のコンポーネントが内部で処理していた img 要素に渡す props を取得するためのメソッドです。

簡単な例として以下のように使用できます。

import { unstable_getImgProps as getImgProps } from "next/image";

export default function Page() {
  const { props } = getImgProps({
    src: "/sample.png",
    alt: "sample",
    width: 800,
    height: 400,
  });

  return <img {...props} />;
}

具体的な実用例としては大きく 3 つに分けられそうです。

  • next/image を使わずに img 要素を使いたい
  • next/image を使いたいが picture 要素を使いたい
  • 直接 img 要素を使わずに画像を表示したい

またここから先の具体的な実装パターンではあらかじめ getImgProps をエクスポートするファイルを用意している前提で進めていきます。

src/lib/getImgProps.ts
export { unstable_getImgProps as getImgProps } from "next/image";

next/image を使わずに img 要素を使いたい

このケースでは以下のようなメリットがあります。

  • サーバーコンポーネントで機能する

サーバーコンポーネントで機能する

next/image はサーバーコンポーネントでは機能しません。
これは、placeholder 属性による画像読み込み中の処理や、onLoadonError 属性を処理するために useState などの ReactHooks やブラウザ API を使用しているためです。

一方で getImgPropsnext/image の内部で使用している img 要素に渡す props を取得するだけなので、サーバーコンポーネントでも機能します。
またご存知の方もいるかとは思いますが、v13.0.0 から next/image は元々 next/future/image として提供されていたものに差し変わりました。
このタイミングでほぼ単純な img 要素になっています。
遅延読み込みは loading="lazy" 属性で実現していたり、layout 属性で色々な CSS が適用されていたのもこのタイミングで削除されています。(placeholder 属性などの影響でゼロではありません)
そのため使い方によっては実は next/image を使わずに img 要素を使う今回の方法が適している場合もあるかもしれませんね。

実装例

src/components/ImgTag.tsx
import { getImgProps } from "@/lib/getImgProps";
import { ImageProps } from "next/image";

function ImgTag(props: ImageProps) {
  return <img {...getImgProps(props).props} />;
}

export default ImgTag;
src/app/page.tsx
import ImgTag from "@/components/ImgTag";

import imgSrc from "@/images/sample.jpg";

export default function Home() {
  return (
    <main>
      <ImgTag src={imgSrc} alt="" />
    </main>
  );
}

next/image を使いたいが picture 要素を使いたい

このケースでは以下のようなメリットがあります。

  • アートディレクションができる
  • Light/Dark モードに対応できる

アートディレクションができる

ここでいうアートディレクションとは、画面サイズに応じて表示される画像を切り替えることを指します。
例えば、デスクトップでは横長の背景画像を表示している場合、モバイルで同じ画像を表示すると縮小されダサくなることは予想できます。
これをモバイルでは、正方形もしくは縦長の画像を表示するような手法です。

next/image は直接このようなことはできないため、picture 要素を使う必要があります。

(個人的には getImgProps はアートディレクションできるようになることが一番のメリットだと思っています)

Light/Dark モードに対応できる

Light/Dark モードに対応したコンポーネントもアートディレクションと同じような理由で picture 要素を使う必要があります。

厳密には CSS の display:none を使用すれば next/image でも実現できますが、1 つの画像を表示するために複数のコンポーネントが必要となりオーバーヘッドも少なからず存在する点やブラウザのネイティブ機能を活用できていない点から、picture 要素を使うほうが望ましいと考えています。

実装例

アートディレクションと Light/Dark モードに対応したコンポーネントはどちらもやることはほぼ同じなため、アートディレクションに対応したコンポーネントを例に挙げます。

src/components/ArtDirection.tsx
import { ImageProps } from "next/image";

import { getImgProps } from "@/lib/getImgProps";

type Src = ImageProps["src"];

type ArtDirectionProps = {
  src: {
    desktop: Src;
    mobile: Src;
  };
} & Omit<ImageProps, "src">;

function ArtDirection({ src, ...props }: ArtDirectionProps) {
  const { props: imgProps } = getImgProps({ ...props, src: src.mobile });
  const {
    props: { srcSet: desktopSrcSet },
  } = getImgProps({ ...props, src: src.desktop });

  return (
    <picture>
      <source media="(min-width: 768px)" srcSet={desktopSrcSet} />
      <img {...imgProps} />
    </picture>
  );
}

export default ArtDirection;
src/app/page.tsx
import ArtDirection from "@/components/ArtDirection";

import imgMobileSrc from "@/images/sample-mobile.jpg";
import imgSrc from "@/images/sample.jpg";

export default function Home() {
  return (
    <main>
      <ArtDirection
        src={{ desktop: imgSrc, mobile: imgMobileSrc }}
        alt=""
        sizes="100vw"
      />
    </main>
  );
}

直接 img 要素を使わずに画像を表示したい

ここでは img 要素を使わずに画像を表示したい場合について考えます。
具体的には CSS の background-imageimage-set() や、canvas 要素、new Image() で画像を表示したい場合です。

実装例

ここでは CSS の background-imageimage-set() を例に挙げます。
他の方法についても同様に getImgProps を使って srcSet または src を取得し、適切な形式にフォーマットすれば実現できます。

src/components/BackgroundImage.tsx
import { getImgProps } from "@/lib/getImgProps";
import { ImageProps } from "next/image";

type BackgroundImageProps = Omit<ImageProps, "sizes" | "placeholder">;

function BackgroundImage(props: BackgroundImageProps) {
  const backgroundImageUrl = getImgProps(props)
    // 解像度ごとの画像候補文字列に分割
    .props.srcSet?.split(", ")
    // 画像URLと解像度に分割
    .map((src) => src.split(" "))
    // CSSの`image-set`形式にフォーマット
    .map(([src, width]) => `url(${src}) ${width}`)
    .join(",");

  return (
    <div
      className="w-full aspect-[3/2] bg-cover bg-center"
      style={{
        backgroundImage: `-webkit-image-set(${backgroundImageUrl})`,
      }}
    />
  );
}

export default BackgroundImage;
src/app/page.tsx
import BackgroundImage from "@/components/BackgroundImage";

import imgSrc from "@/images/sample.jpg";

export default function Home() {
  return (
    <main>
      <BackgroundImage src={imgSrc} alt="" />
    </main>
  );
}

実装イメージ

以下のURLにて 上記のパターンを実装しているので検証ツールなどでHTMLがどうなるか見ていただくとより理解しやすいと思います。
https://next-get-img-props.vercel.app/

https://github.com/dc7290/next-get-img-props

まとめ

今回は next/imageunstable_getImgProps について紹介しました。
現在は unstable なメソッドなため本番利用については注意が必要ですが、Next.js の画像最適化を next/image 以外で活用できるようになりますので、安定版としてリリースされるのが待ち遠しいですね。

参考

https://nextjs.org/docs/app/api-reference/components/image

https://github.com/vercel/next.js/pull/51205

Discussion