🌄

next/image でアスペクト比不明の動的な画像を受け入れる

2022/12/26に公開

やりたいこと

領域を固定せず、任意の大きさの画像を表示するUIを想定しています。
この場合に問題になるのが CLS (Cumulative Layout Shift) という指標です。何もないところに突然画像が表示されて、画面がガクッとズレると「CLSスコアが悪い」と表現されます。こちらをなるべく解消して、ユーザー体験を良くしていきましょう。

方針

  • 画像の読み込みが完了するまで、アスペクト比を固定したローディング画像を表示する
  • 画像の読み込みが完了したら、読み込んだ画像のアスペクト比で表示する

実装

まずは全体のコードです。

import { useState } from 'react';
import NextImage from 'next/image';

type OnLoadingCompleteResult = { naturalHeight: number; naturalWidth: number };

const DynamicImage = (props: {
  /** 画像パス */
  imagePath: string;
  /** ローディング時に表示する画像のアスペクト比 */
  loadingImageAspectRatio: number;
  /** ローディング時に表示する画像パス */
  loadingImagePath: string;
}) => {
  const [aspectRatio, setAspectRatio] = useState(0);
  const onLoadingComplete = (e: OnLoadingCompleteResult) => {
    setAspectRatio(e.naturalWidth / e.naturalHeight);
  };

  return (
    <div
      style={{
        aspectRatio: `${aspectRatio || props.loadingImageAspectRatio}`,
        position: 'relative',
      }}
    >
      <NextImage
        src={props.imagePath}
        layout="fill"
        placeholder="blur"
        blurDataURL={props.loadingImagePath}
        objectFit="cover"
        onLoadingComplete={(e) => onLoadingComplete(e)}
      />
    </div>
  );
};

next/imageheight, width を指定するのが基本です。先に表示領域を決めておけば画面のズレが発生することはないので、良いですね。ただし、今回はアスペクト比不明の画像を受け入れたいので、これらを固定することはできません。
以下、ポイントを説明していきます。

ポイント1: aspect-ratio を指定する

2021年より各ブラウザでサポートされるようになった CSS property です。対応状況は下記の通りで、執筆時点で大半のブラウザでサポートされています。
https://caniuse.com/mdn-css_properties_aspect-ratio

こちらで next/image をラップしてあげることで、ローディング時に表示する画像のアスペクト比を固定することができます。

ポイント2: onLoadingComplete を利用する

画像の読み込みが完了すると、画像の naturalWidthnaturalHeight を返してくれます。つまり、読み込んだ画像の縦横比がわかります。こちらを上述のローディング画像のアスペクト比と差し替えてあげることで、比率を保って表示することができます。

補足

あまりに柔軟に画像を受け入れすぎると、CLSの悪化を防ぐことはできません。例えばアスペクト比1〜2まで許容してしまうと、ローディング画像のアスペクト比を中間の1.5にしても大きなズレが発生する可能性があります。そんなケースではそもそもプロダクトの設計を見直すべきです。1.4〜1.6まで、というように一定の制限を設けると良いでしょう。

Discussion