2021/09/01

next/imageって何ができるん。

@ 酒井悠宇

今日はnext/imageについて調べていきたいと思います。
nextで画像を自由に扱えるようになりたい!!
https://www.forcia.com/blog/001561.html

next/imageとは?


「 next/imageとは、画像サイズと拡張子をデバイスとブラウザに応じて最適な形で出し分けてくれるReactコンポーネントのこと」

画像の最適化は重要だが手間がかかる


画像ファイルは一般的なWebページの全体のバイト数の半分を占める。最適化されていない画像の送受信や描画はページ表示の遅れにつながり、UX(ユーザーエクスペリエンス)の悪化につながる。
ECサイト等のように、多くの商品画像を表示する必要があるウェブサイトの場合、この問題は特に重要。

next/imageのRFC等では、最適化されていな画像とは何かと、そのUXへの悪影響について、以下の点が指摘されている。

RFCとは?
https://gimo.jp/glossary/details/rfc.html

RFCとは、インターネットに関する仕様や規則等を定めた文書であり、インターネットにおける技術標準を示すための文書です。RFCはIETFというインターネット関連の技術を標準化するための組織によって策定され、定期的に発行されています。名前はRequest for Commentsの略称であり、この言葉は「コメントを募集しています」という意味です


1.画像サイズ:通信される画像のサイズの実際に表示される画像サイズがあっていない。


例えば、スマホの画面に100 x 100pxの画像を<img>タグで表示したいとする。この時、大きすぎるサイズの画像500 x 500pixelの画像を送ってしまうと、無駄な通信が発生して画面描画が送れる。これは全体的なUXの低下につながる。

2.拡張子:軽量な拡張子の画像が使われていない。


モダンな拡張子、例えばwebpはjpeg.pngに比べて30%程度軽量。従って、jpeg、png画像をこれらの拡張子に置き換えることで通信量の削減ができる。

3.タイミング:viewport外の画像を読み込んでいる。


初期描画でページ内の全ての画像を読み込むと、表示にかかる時間が不必要に伸びてしまう。速度と表示を両立させるためには、初期描画ではviewport内の画像のみを、それ以外の画像は、viewportが近づいたタイミングで順次読み込む。(遅延ローディング)

これらの課題の解決法


これらの課題は有名な対応法がある。例えば画像サイズの最適化に関しては、<img>タグでsrcsetを設定すれば、ブラウザが複数の画像から最適なサイズの画像を読み込みませるようにすることができる。
拡張子の最適化は、jpeg, pngを片っぱしからwebpに変換すれば対応できる。
遅延ローディングに関しても、intersection observerを使った実装などがよく知られている。

しかし、上記の画像の最適化はweb全体を見ると十分に浸透しているとはいえない。
これには以下の理由が考えられる。

1.ブラウザ間の差異を考慮する必要がある。


上記のモダンな拡張子webpなどは、一部のブラウザではサポートされていないため、これらのブラウザのサポートと画像拡張子の最適化を同時にしようとすると、ブラウザを見て返却する拡張子を変化させる必要がある。
また、遅延ローディングにはさまざまな実装方法が知られているが、実装方法によってはうまく機能しないブラウザなどもあり注意が必要。また、ブラウザによって画像サイズを出し分ける場合、素朴には各画面に対してsrcsetと各サイズの準備をする必要があり、実装の手間がかかる。

2.外部サーバーから画像を取得する場合、画像の最適化とキャッシュ機能を担う中継サーバーが必要になる。


外部サーバーから取得した画像を最適化する場合、外部サーバーから画像を取得し、画像の最適化をしてブラウザに返却するような中間のサーバーが必要になる。

画像の最適化に真剣に取り組もうとするとこれらの課題をクリアしながら開発する必要がある、そのコストは決して少なくない。

これらの解決策として登場したのがnext/image。

next/imageの概要と基本的な使い方。


next/imageの導入は非常に簡単。タグをReactコンポーネントに置き換えるだけで画像サイズと拡張子の適切な出し分けができるようになる。付属ライブラリなどのインストールなども不要。

// Case A. 通常の画像コンポーネント
const UnoptimizedImage = () => <img src="{`/river.jpg`}" width="{360}" height="{240}" />;

// Case B. next/iamgeを使った画像コンポーネント
import Image from "next/image";

const OptimizedImage = () => (
  // <img="" />を <Image />に置き換えるだけ!!
  <Image src={`/river.jpg`} width={360} height={240} />
);


<img>を使った場合も<Image>コンポーネントを使った場合も同じ画像が表示される。一方で、画像取得のリクエストや生成されるDOM要素は大きく異なり、<Image>コンポーネントを使った場合、public配下に何の最適化もしていない画像を配置しても、最適な画像サイズが最適な拡張子で遅延読み込みされるようになる。

<Image>コンポーネントから生成されるDOM要素


<Image>コンポーネントから生成されるDOM要素は以下のようになる。<img>に加えて<img>をラップするようなDOM要素が生成される。

// Case A. 通常の画像コンポーネントから生成されるDOM要素
<img src="/river.jpg" width="360" height="240" />


// Case B. Imageコンポーネントから生成されるDOM要素
<!-- レイアウトを整える用のラッパーDOM要素 -->
<div
  style="
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    position: relative;
    box-sizing: border-box;
    margin: 0;
  "
>
  <div style="box-sizing: border-box; display: block; max-width: 100%">
    <img
      style="max-width: 100%; display: block"
      alt=""
      aria-hidden="true"
      role="presentation"
      src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYwIiBoZWlnaHQ9IjI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiLz4="
    />
  </div>
  <!-- 画像のDOM要素 -->
  <!-- srcが/_next/image/下のパスに置き換わり、decoding, srcsetが設定されている ! -->
  <img
    src="/_next/image?url=%2Friver.jpg&w=1080&q=75"
    decoding="async"
    style="
      visibility: visible;
      position: absolute;
      inset: 0px;
      box-sizing: border-box;
      padding: 0px;
      border: none;
      margin: auto;
      display: block;
      width: 0px;
      height: 0px;
      min-width: 100%;
      max-width: 100%;
      min-height: 100%;
      max-height: 100%;
    "
    srcset="
      /_next/image?url=%2Friver.jpg&w=384&q=75  1x,
      /_next/image?url=%2Friver.jpg&w=750&q=75  2x,
      /_next/image?url=%2Friver.jpg&w=1080&q=75 3x
    "
  />


<Image>コンポーネントにおける画像取得の仕組み


<Image>コンポーネントにおける画像取得の仕組みは大まかに以下のようになっている。

  • 生成されたDOM要素でdecoding = async が設定されているため、画像のデコード処理が非同期的にバックグラウンドで処理されるようになる。<Image>コンポーネントはデフォルトで遅延ローディングになっており、対象画像がviewportに近づくとローディングが始まる。内部的にはintersectionObserverを使って対象画面との相対位置を監視している。


  • 上の生成されたDOM要素を見るとわかるように、<Image>コンポーネントから生成された<img>タグにはsrcsetが設定されている。これにより、ブラウザはsrcsetで設定されたw(width)とx(ピクセル密度)が異なる3つの画像からブラウザ幅に応じて最適なものを選んでリクエストする。例えば、PCの画面の場合だと<Image>コンポーネントに設定されたwidth=360に最も近い画像幅w-384を持つx=1のケースが選ばれ、リクエスト(/_next/image?url=%2Friver.jpg&w=384&q=75)が送られる。


  • /_next/imageはnextのビルド時にできる画像サーバー。リクエストを受けた画像サーバーは、リクエスト元のブラウザとクエリパラメーターから画像サイズと拡張子を適切なものに変換し、プラウザに返却する。上の例だと、Chromeはwebp対応しているため、webpでwidth=384, height=254の画像ファイルをクライアントに返却する。一点注意するべきは、<Image>による画像の変換は、ビルド時ではなく、ラインタイムで行われると言うこと。これにより画像のレスポンス時間は長くなるものの、画像数の増加によるビルド時間の増加を防ぐことができる。


<Image> を使うことにより、画像のサイズ・拡張子・読み込みタイミングの最適化と言う冒頭で挙げた3つの問題が解消されていることがわかる。
また、/_next/imageにできる画像サーバーにはキャッシュ機能があり、変換されたwebp画像が.next/cache/images/配下に保持され、同じリクエストに対してはwebpへの変換なしでクライアントに返却される、ブラウザに画像がある場合は/_next/imageでバリデーションした後に304コードを返却することでブラウザのキャッシュを利用する、などのようなことをしている。

「304 (Not Modified)」とは、リクエストしたリソースが更新されていないことを示すステータスコードです。これでWebサイトが表示されている場合は、ブラウザ内のキャッシュに残っているコンテンツを使ってWebページを表示していることになります。ちなみに、キャッシュとは一度アクセスしたサイトのデータを一時的に保管しておく仕組みです。


<Image>はブラウザに応じて、適切に拡張子を選んでくれる。
例えば、webpに対応しているChrome, Firefoxに対してはwebpが、webp非対応のIEに対してはjpegがそのまま返却される。

コンポーネントのオプション


<Image>コンポーネントには豊富なオプション引数が存在する。

<Image
  src={`/river.jpg`} // ソースファイル, string
  width={420} // 表示幅, number
  height={280} // 表示高さ, number
  quality={75} // 画質, number
  priority={false} // 表示の優先度, boolean
  loading={"lazy"} // 遅延ロードするかどうか, "lazy" | "eager"
  unoptimized={false} // 最適化するかどうか, boolean
  layout={"fixed"} // レイアウト, "fill" | "fixed" | "intrinsi| "responsive"
  objectFit={"contain"} // layout='fill'の場合のobject-fit
  objectPosition={"50% 50%;"} // layout='fill'のobject-position
/>


これらの他に、例えば画像のalt属性など、<img>タグに設定できる属性は<image>コンポーネントのpropsとして設定することができる。但し、style, srcSet, decodingは例外で、設定したとしても<Image>コンポーネント内の<img>タグのpropsを設定する際に上書きされてしまう。

必須引数

1.src: 画像のソースファイル

  • 型: string

2.width: 画像の幅

  • 型: number
  • layout='fill'の時以外は必須

3.height: 画像の高さ

  • 型: number
  • layout='fill'の時以外は必須


widthやheightは通常の<img>タグと異なり、必須の引数となっている。widthやheightの設定されていない画像は、CLSの悪化を引き起こすが、<Image>コンポーネントでは開発者が自然とそれを避けられるように設計されている。

CLSとは、 Common Language Specification(共通言語仕様)のこと。


priority, loading: 表示タイミング・表示の優先度についての任意引数


1.priority: preloadするかどうかのフラグ

  • 型: boolean
  • デフォルト値: false
  • trueの場合は、ページ遷移時にpreloadされる。


2.loading: 遅延ローディングをするかどうかのフラグ

  • 型: "lazy" | "eager"
  • デフォルト値: lazy
  • loading=lazyの場合はviewportから計算された値でローディングを開始し、loading=eagerの場合はviewportの位置に関わらず、ページ遷移した時にローディングを開始する。


上で説明したように、デフォルト設定では画像が遅延ロードされるが、遅延ロードを有効にしたくないケースもある。例えば、サイズが大きくローディングに時間がかかる画像や、トップページのヒーローイメージのようにファーストビューですぐに表示したい画像などだ。これらのケースではpreload=true や loading='eager' の設定が有効。

unoptimized, quality: 最適化の有無と画質についての引数


1. unoptimized: 最適化するかどうかのフラグ

  • 型: boolean
  • デフォルト値: false
  • unoptimized = true の場合、生成されるhtmlでは<img src='/river.jpg'>のようになり、srcsetも生成されない。このため、_next/imageにリクエストはされず、最適化された画像がクライアントに表示されることもない。


2.quality: 画質

  • 型: number(1 ~ 100 の数値)
  • デフォルト値: 75
  • qualityを変化させると、nextの画像サーバー/_next/image/へのリクエストへのクエリパラメーターqが変化する。qualityを1(最低値)、又は75(デフォルト値)、又は100(最高値)にした場合のことを考えてみる。画像によっては、q=1だと画質の荒さが気になるけど、q=75 と、 q=100 はほとんど遜色なく置き換えてもいいように感じる場合があると考えらえれる。


qualityによって画像サイズが劇的に変化するため、このオプションは背景画像など、サイズが大きくローディング時間を短縮したい状況で使えそう。unoptimized=trueと設定するべき状況として、RFCでは、next/imageに対応していないloaderの画像を取得する場合などが挙げられている。公式ドキュメント

layout, objectFit, objectPosition: 画像の幅と高さなどのレイアウトについての引数


<Image>コンポーネントでは、画像のレイアウトに対しても豊富なオプションが提供されている。

1.layout: viewportを変更した時のレイアウトを表す

  • 型: "fill" | "fixed" | "intrinsic" | "responsive"
  • デフォルト値: intrinsic
  • layout='fixed': viewportの幅によらず、設定されたwidth, heightの画像を表示する。
  • layout='intrinsic': widthがviewport幅よりも小さい場合は、viewport幅に合わせて小さくなるが、画像の幅がviewport幅よりも大きい場合はwidthの値に設定される。
  • layout='responsive': viewport幅に依存して画像幅が変化する。layout='intrinsic'の場合と異なり、画像の幅がviewport幅よりも大きい場合はviweport幅に合わせて画像幅が増加する。
  • layout='fill': 親のDOM要素のheight, width に合わせて画像の幅と高さが設定される。


2.obujectFit, objectPosition:

  • layout='fill' と同時に使用され、親のDOM要素内での相対位置を表すオプションobject-fit, object-positionの値を設定する。


最後に

はい、と言うわけで今日はnext/imageについて色々調べてみました。
ただ何となく何となく使っていたんですが、next/imageがどんなことをしているのかが雰囲気わかりました。
最後までごらんいただきありがとうございました!

© 2021 powerd by UnReact