Closed8

next/image をソースコードから理解したい。(クライアントサイド編)

ほつかいほつかい

基本的なユースケースとレンダリングされるHTMLを見る

sizesを指定しないとき

sample.tsx
      <Image
        alt=""
        width={300}
        height={400}
        src={
          "https://picsum.photos/1000/1000"
        }
      />
生成されたHTMLにフォーマットをかけたもの
<img
  alt=""
  loading="lazy"
  width="300"
  height="400"
  decoding="async"
  data-nimg="1"
  style="color: transparent"
  srcset="
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=384&amp;q=75 1x,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=640&amp;q=75 2x
  "
  src="/_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=640&amp;q=75"
/>

sizesを指定するとき

      <Image
        alt=""
        width={300}
        height={400}
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        src={
          "https://picsum.photos/1000/1000"
        }
      />
生成されたHTMLにフォーマットをかけたもの
<img
  alt=""
  loading="lazy"
  width="300"
  height="400"
  decoding="async"
  data-nimg="1"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  srcset="
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=256&amp;q=75   256w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=384&amp;q=75   384w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=640&amp;q=75   640w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=750&amp;q=75   750w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=828&amp;q=75   828w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1080&amp;q=75 1080w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1200&amp;q=75 1200w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1920&amp;q=75 1920w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=2048&amp;q=75 2048w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=3840&amp;q=75 3840w
  "
  src="/_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=3840&amp;q=75"
  style="color: transparent"
/>
ほつかいほつかい

srcsetから理解していく

生成しているところ。
https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/get-img-props.ts#L208-L229

(余談) ブラウザごとのsrcsetの挙動
    // It's intended to keep `src` the last attribute because React updates
    // attributes in order. If we keep `src` the first one, Safari will
    // immediately start to fetch `src`, before `sizes` and `srcSet` are even
    // updated by React. That causes multiple unnecessary requests if `srcSet`
    // and `sizes` are defined.
    // This bug cannot be reproduced in Chrome or Firefox.

GPTによる翻訳

srcを最後の属性に保つのが意図されています。なぜなら、Reactは属性を順番に更新するからです。もしsrcを最初の属性に保持すると、SafariはsrcSetとsizesがReactによって更新される前に即座にsrcを取得し始めます。これにより、srcSetとsizesが定義されている場合に複数の不必要なリクエストが発生します。このバグはChromeやFirefoxでは再現できません。

初めて知った...

loadergetWidthsを見てみよう

ほつかいほつかい

loader (ドキュメント)

https://github.com/vercel/next.js/blob/8e2034573e789e776d8edac74dfbee376a9ada08/packages/next/src/shared/lib/get-img-props.ts#L286

Imageコンポーネントに指定したloaderあるいはdefaultLoaderが使われる。
src, width, qualityを渡すと画像urlを返す関数

(余談)loaderの読み込みについて
ほつかいほつかい

getWidths

https://github.com/vercel/next.js/blob/8e2034573e789e776d8edac74dfbee376a9ada08/packages/next/src/shared/lib/get-img-props.ts#L136-L177

deviceSizes, allSizes

sizesの指定がないとき

widthwidthの2倍より大きいものを1つずつallSizesから選択している。
※ 条件に合うものがないときはallSizesから最大のものが選ばれる

const  deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840]
const imageSizes = [16, 32, 48, 64, 96, 128, 256, 384]
const allSizes =  [...deviceSizes, ...imageSizes].sort((a, b) => a - b) // [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840] 
const config = { allSizes, deviceSizes }

var width = 500
getWidths(config, width) // => {width: [640, 1080], kind: 'x'}

var width = 5000
getWidths(config, width) // => {width: [3840], kind: 'x'}

sizesの指定があるとき

sizesって?

(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw
のとき

  • ウィンドウサイズが768ピクセル以下の場合、画像の幅はビューポートの100%に設定される。
  • ウィンドウサイズが768ピクセルより大きく、1200ピクセル以下の場合、画像の幅はビューポートの50%に設定される。
  • ウィンドウサイズが1200ピクセルより大きい場合、画像の幅はビューポートの幅の33%に設定される。

deviceSizesとsizesから、最小の画像幅のピクセルサイズを計算する。
今回の例だとsizesの最小は33vwdeviceSizesの最小は640なので、
640 x 0.33211pxがあり得る最小の画像幅になる。
戻り値はallSizesからそれより大きいものがすべて選択される。

const sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
getWidths(config, 500, sizes) // => {widths: [256,384,640,750,828,1080,1200,1920,2048,3840], kind:"w"}
ほつかいほつかい

priorityを理解する

生成されたHTMLにフォーマットをかけたもの
<link
  rel="preload"
  as="image"
  imagesrcset="/_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=384&amp;q=75 1x, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=640&amp;q=75 2x"
  fetchpriority="high"
/>
<!-- 中略 -->
<img
  alt=""
  fetchpriority="high"
  width="300"
  height="400"
  decoding="async"
  data-nimg="1"
  style="color: transparent"
  srcset="
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=384&amp;q=75 1x,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=640&amp;q=75 2x
  "
  src="/_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=640&amp;q=75"
/>
sizesの指定があったとき
<link
  rel="preload"
  as="image"
  fetchpriority="high"
  imagesrcset="/_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=256&amp;q=75 256w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=384&amp;q=75 384w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=640&amp;q=75 640w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=750&amp;q=75 750w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=828&amp;q=75 828w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1080&amp;q=75 1080w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1200&amp;q=75 1200w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1920&amp;q=75 1920w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=2048&amp;q=75 2048w, /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=3840&amp;q=75 3840w"
  imagesizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<!-- 中略 -->
<img
  alt=""
  fetchpriority="high"
  width="300"
  height="400"
  decoding="async"
  data-nimg="1"
  style="color: transparent"
  srcset="
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=256&amp;q=75   256w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=384&amp;q=75   384w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=640&amp;q=75   640w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=750&amp;q=75   750w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=828&amp;q=75   828w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1080&amp;q=75 1080w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1200&amp;q=75 1200w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=1920&amp;q=75 1920w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=2048&amp;q=75 2048w,
    /_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=3840&amp;q=75 3840w
  "
  src="/_next/image?url=https%3A%2F%2Fpicsum.photos%2F1000%2F1000&amp;w=3840&amp;q=75"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

ソースコードはここらへん
https://github.com/vercel/next.js/blob/bb7577aa4476a36ed1d1ea5cb122237443f2b513/packages/next/src/client/image-component.tsx#L412-L417

このスクラップは2ヶ月前にクローズされました