🖼️

next-optimized-images で next/image っぽいものをつくる

7 min read

next/image での画像最適化が最近のホットなトピックではありますが、見た感じ、next/imageは画像の最適化をサーバサイドで処理する前提のコンポーネントのようです。

Next.jsでサーバを立てられる場合は使っていきたいのですが、個人的に案件でそういう構成になっていることはあまりないです。
そこで、next export で使える画像最適化についてまとめてnext/imageっぽいコンポーネントを作ってみようと考えました。

一応、公式の next export での next/image の使用法についてのページはこちら です。

7/10: 一部のモジュールのインストールを書き忘れていたので追加、next.config.jsについても追記しました。

7/23: フォントファイルを読み込むとエラーがでてしまう問題について、暫定の解決策を追記しました。

前提

今回はnext/imageのソースを見て、ほぼマルパクリな感じでやっていきます。
IE対応はしません。
Next.jsのバージョンは 11.0.1です。

やること

  • webp対応 / 非対応ブラウザの処理
  • 画像のsrcSetの生成
  • 遅延読み込み
  • background-image の対応

画像最適化は next-optimized-images 、遅延読み込みは 普通に(next/imageにならって)Intersection Observerでやっていきます。

リポジトリ、デモURL

https://github.com/itomise/next-image-optimization-demo/blob/master/src/components/atoms/Image.tsx

https://next-image-optimization-demo.vercel.app/my-complete-img/

使うモジュール

next-optimized-images にまとまっていますが、各拡張子ごとに使うモジュールを別途インストール必要があります。
今回は、全部で以下をインストールしました。

  • next-optimized-images
  • imagemin-mozjpeg
  • imagemin-pngquant
  • webp-loader
  • responsive-loader
  • sharp
  • raw-loader

インストールは以下コマンドをお使いください

yarn add -D next-optimized-images imagemin-mozjpeg imagemin-pngquant webp-loader responsive-loader sharp raw-loader

next.config.js

next-optimized-imagesのconfigをここで定義しています。
responsive-loaderで生成する画像サイズの種類を一括で適用できます。

const optimizedImagesConfig = {
  optimizeImagesInDev: true,
  removeOriginalExtension: true,
  responsive: {
    adapter: require('responsive-loader/sharp'),
    sizes: [640, 960, 1200, 1800],
  },
}
pngの最適化モジュールについて

next-optimized-imagesのgithubでは imagemin-optipng が推奨のような感じになっていますが(imagepngquantが alternative になっているので)、どっちのほうがいいのかなーと思って軽く検証してみました。

結果的に、

  1. 2.51MBの画像(無加工)
  • imagemin-optipng:1.5MB
  • imagemin-pngquant:721KB
  1. 546KBの画像(1をTinypngのサイトで軽量化したもの)
  • imagemin-optipng:559KB
  • imagemin-pngquant:612KB

という感じの結果になりました。
画像サイズが大きいとき、optipngがpngquantよりも2倍くらいサイズが大きくなってしまいました。
この辺の画像の最適化について調べたことがなくあまり評価するポイントがわからなかったため、optipngとpngquantでどっちがいいか調べてみたところ

https://www.saashub.com/compare-pngquant-vs-optipng
https://pointlessramblings.com/posts/pngquant-vs-pngcrush-vs-optipng-vs-pngnq/

のサイトで pngquantのほうがいいよー的なことが書いてあったので、今回は imagemin-pngquant のほうを採用することにしました。
(品質が落ちすぎてるなど今後気づいたりしたら変えるかもしれません)

next-optimized-imagesのresponsive-loader の不具合

responsive-loader は画像をレスポンシブでサイズを変えて表示できるように複数の画像を生成するモジュールですが、pngの最適化と併用できないという不具合があるっぽいです。
https://github.com/cyrilwanner/next-optimized-images/issues/179

webpの場合は十分軽量化されているようなので大丈夫そうでした。

※以下、画面幅で表示する画像サイズを複数用意する方法を「サイズの最適化」、単純に画像の容量を落とすことを「画像の最適化」と言うことにします。(表現がわからず)

併用するとサイズの最適化はしてくれても画像の最適化はしてくれないようになって、画面幅が大きいともちろん元の画像サイズのままになってしまいます。

だったら、サイズの最適化はやめて画像の最適化だけ行ったほうが容量は小さくなるので、

webp対応の場合:サイズの最適化 & webp化
webp非対応の場合: 画像の最適化のみ

という方法でやりたいと思います。
(まじで Safari 全バージョンでwebp対応してくれ)

webpack5について

Next.js 11 からwebpack5がデフォルトになったのですが、next-optimized-imagesはまだwebpack5に対応していません。
今回試しに上げてみたらなんとか動いたので、一旦このまま様子を見たいと思います。

font読み込み時の不具合について(21/07/23追記)

こちら に issueで上がっていますが、デフォルトだとフォントファイルも next-optimized-images のwebpack が通ってしまうようになってしまいます。
コメントでもあがっていますが、next.js自体のconfigに以下の記述を加えて、フォントファイルは raw-loader を使うように指定することで解決することができました。

  webpack: (config) => {
    config.module.rules.push({
      test: /(\.ttf|\.otf|\.woff|\.woff2)$/,
      use: 'raw-loader',
    })
    return config
  },

webp対応 / 非対応ブラウザの処理、画像のsrcSetの生成

resize で複数サイズ生成、format=webpでwebp化ができます。
(デフォルトだと ?webp ですが responsive-loaderの場合は format=webpになるので注意してください)
webpだけresizeがついてるところに関しては ↑の「responsive-loader の不具合」を参照してください。

const webp = require(`../../../public${src}?resize&format=webp`)
const img = require(`../../../public${src}`)

pictureタグで二つを出し分けることで、非対応ブラウザは普通のimgを表示するようにします。

遅延読み込み

IntersectionObserverのhooksはnext/imageと少し違うのですが、こちら のコードを使っています。(対して変わりませんが)

表示領域にはいったら、imgタグのsrc attribute にurlを差し込む形です。

const entry = useIntersectionObserver(ref, {
    rootMargin: '200px',
    freezeOnceVisible: true,
})

const isVisible = !isLazy || !!entry?.isIntersecting

// ...

let imgAttributes = {
    width,
    height,
    src:  'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
    srcSet: '',
}
let webpImgAttributes = {
srcSet: '',
}
if (isVisible) {
    imgAttributes = {
      ...imgAttributes,
      src: img,
    }
    webpImgAttributes = {
      srcSet: webp.srcSet,
    }
}

// ...

      <picture css={style.picture}>
        <source {...webpImgAttributes} type="image/webp" />
        <img
          {...rest}
          {...imgAttributes}
          decoding="async"
          alt=""
          className={className}
          ref={ref}
          style={imgStyle}
          css={style.img}
        />
      </picture>

background-image の対応

まず、デフォルトの画像の説明

next/imageの実装だと、padding-topで領域を確保する用のdivを用意して imageをabsoluteで置くという感じになっていたので、同じようにしました。
next/imageと違うところは、一部のスタイルをインラインではなく Emotion で、クラスの付け替えで表現するようにしています。(そっちのほうがわかりやすそうだったので)

padding-top用のsizerStyle というstyleを用意して、background用のimageではない場合はこちらで表示するようにしています。

if (
    typeof widthInt !== 'undefined' &&
    typeof heightInt !== 'undefined' &&
    layout !== 'fill'
  ) {
    // default : <Image src="i.png" width="100" height="100" />
    const quotient = heightInt / widthInt
    const paddingTop = isNaN(quotient) ? '100%' : `${quotient * 100}%`
    sizerStyle = { display: 'block', boxSizing: 'border-box', paddingTop }
  }

background-imageの場合

sizerStyleをなくすことにより、ただの absolute の image になるので、コンポーネントをラップする側のdivで position: relative 、 Heightを確保することでbackgrond-image風な表示になっています。
background-sizeやbackground-positionなどは object-fit と object-positionにより調整できるようにしてあります。

// ...
        <div css={style.header}>
          <Image
            src={imgList[3]}
            layout="fill"
            objectFit="cover"
            objectPosition="center center"
          />
        </div>
// ...

const style = {
  header: css`
    position: relative;
    width: 100%;
    height: 320px;
  `,
}

デモサイト

上記で記載しましたがあらためて。
vercelにデプロイしてます。

https://next-image-optimization-demo.vercel.app/

HTML標準の機能で img の loading attribute について少し気になっていたので検証がてらページを作ってみています。
これが標準化されると楽ですね。

最後に

全体的にだいぶ雑だったり、まだ placeholder で読み込み前に blurの画像を表示する機能や、 priority で prefetchしておきたい画像は head 内に書いておく機能あたりは書いていないので、今後追加していきたいと思います。
なにか間違ってたりしたら教えてください。

参考記事

https://nextjs.org/docs/basic-features/image-optimization
https://coliss.com/articles/build-websites/operation/work/native-image-lazy-loading.html
https://spelldata.co.jp/blog/blog-2019-12-19.html
https://oriverk.dev/ja/posts/20200921-optimized-images/