👍

【Next.js14】next/imageによる画像パフォーマンス最適化(blur・lazyローディング)

2024/02/17に公開

はじめに

今回は、Next.jsを使用して画像をblurローディングする方法についてまとめます。
また、デモ動画では高速表示捺せ対画像とそうでないblurの画像の表示比較を行い、違いを確認しています。

前提

今回の実装では、MicroCMSからデータを取得しています。

ただ、個人的には、動的データの場合、next/imageのblurローディングではなく、Suspenceで囲って、画像も含めたプロパティ全てをローディングにしてから表示する実装をすることをおすすめします。

blurローディングはLPなど画像をそれなりに使用する場合で、初期表示時に画像が表示されるが、高速表示の優先度は低い場面等で使用してください。
(ユーザーに画像を読み込んでいるという認識を与えるために使用してください)

まとめると、画像のパス(src部分の値)がはっきりしている静的ファイルに使用することを私は推奨いたします。
(動的ファイルには上記のようにSuspenseを使用したほうがWebアプリでは良いと考えているからです)

※ インスタグラムなど、画像メインのアプリを作成する場合などは画像表示のblurローディングはありなので、原則Supenseという認識です

パフォーマンス最適化について

先程、以下のようなことを説明しました。

blurローディングはLPなど画像をそれなりに使用する場合で、初期表示時に画像が表示されるが、高速表示の優先度は低い場面等で使用してください。
(ユーザーに画像を読み込んでいるという認識を与えるために使用してください)

パフォーマンス最適化のために追加で解説すると、スクロールしないと見えない画像についてLazyローディングさせるのがよいです。
(next/imageの場合は、priorityというpropsを追加しない場合、すべてlazyローディングになるようです)
https://zenn.dev/nameless_sn/articles/nextjs_image_important_points

まとめると、ユーザーに「この画像は確実に表示されないといけない」画像についてはpriorityを付け、「画像表示はするが画像のローディングである」ということを示したい場合は、blur ローディング、「そもそもスクロールしないと画像があることすらわからない」という画像についてはlazy ローディングというような使い分けをすると良いということです。

ちなみに、コンポーネントをlazyローディングするには、React.lazyを使用します。
https://ja.legacy.reactjs.org/docs/code-splitting.html#reactlazy

このようにすることでブラウザに送られるJSのバンドルサイズを下げることができます。

余談ですが、React19からはRSCでReact.Lazyが実現できるようですし、他にも実装が楽になったり、実装の幅が広がるようなアップデートがあるようです。
(とはいえ、React.Lazy実装しないから知識いらないということではなく歴史を知っておくと実装の際にバッドプラクティスを踏むことがなくなるのでコーディングで不要でも、このようなことは積極的に調べたほうがよいと私は思っています)
https://twitter.com/acdlite/status/1758229889595977824

環境

  • next 14.1.0
  • sharp 0.33.2
  • plaiceholder 3.0.0
  • microcms-js-sdk 2.7.0
  • tailwind-variants 0.1.20

実装

さて、前提が長くなりましたが、実装の解説に移ります。
実装の全体ですが、以下のように実装しました。
(コードを抜粋し、順に解説します)

app/page.tsx
import { getAllBooks } from '@/libs/microcms'
import { BookType } from '@/types/Book'
import { MicroCMSListResponse } from 'microcms-js-sdk'
import Image from 'next/image'
import { getPlaiceholder } from 'plaiceholder'
import { tv } from 'tailwind-variants'

const home = tv(
  {
    slots: {
      base: 'flex flex-wrap justify-center items-center mt-20',
    },
    variants: {
      size: {
        md: { base: 'mt-32' },
      },
    },
  },
  { responsiveVariants: ['md'] },
)

const Home = async () => {
  const { base } = home({ size: { md: 'md' } })

  const { contents: books } =
    (await getAllBooks()) satisfies MicroCMSListResponse<BookType>

  if (!books[0].thumbnail?.url) {
    return null
  }

  const response = await fetch(books[0].thumbnail.url)
  const arrayBuffer = await response.arrayBuffer()
  const buffer = Buffer.from(arrayBuffer)
  const { base64 } = await getPlaiceholder(buffer)

  return (
    <main className={base()}>
      <Image
        src="/default_icon.png"
        width={200}
        height={200}
        priority={true}
        alt="test"
      />
      <Image
        src={books[0].thumbnail.url}
        width={200}
        height={200}
        placeholder="blur"
        blurDataURL={base64}
        alt="test"
      />
    </main>
  )
}

export default Home

まず、blurローディングするためには、next/imageのImageコンポーネントのplacehlderというpropsに"blur"を指定するだけでできます。

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

ただし、上記のpropsのみの指定だけではnext/imageのエラーで「blurDataURLがないよ」というエラーが出ます。

このblurDataURLにはblurローディングに表示させたい画像のsrcを指定します。
つまり、本来表示すべき画像を解像度を下げた状態にして、その解像度を下げた画像のURLを渡す必要があるということです。

もう少しわかりやすく解説します。
例えば上記の実装のbooks[0].thumbnail.urlのblurローディング時に、別の画像を表示する場合を考えてみましょう。
この場合、例えばblurDataURLhoge.pngという静的画像を指定するとします。
この例でいうと、books[0].thumbnail.urlのblurローディング時には、hoge.pngが表示され、books[0].thumbnail.urlの読み込みが終わると、books[0].thumbnail.urlが表示されます。

要するに、placeholder="blurblurDataURLはセットで使用して、初めてblurローディングが実現できるということです。

それでは、抜粋したコードを見ていきましょう。

低解像度画像に変換する処理

このコードでは、books[0].thumbnail.urlをバイナリデータに変換して、plaiceholderというライブラリでbase64文字列にエンコード(変換)された低解像度の画像に変換しています。

const response = await fetch(books[0].thumbnail.url)
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const { base64 } = await getPlaiceholder(buffer)

まずは、ライブラリの導入ですが、公式にあるようにインストールしていきます。
plaiceholdersharpというライブラリで画像変換するためsharpのインストールも必要なようです)

fish
pnpm add sharp plaiceholder

https://plaiceholder.co/docs/getting-started

さらに、上記コードを詳細に解説すると、まず、文字列である画像URL(books[0].thumbnail.url)をBuffer型(バイナリデータ)にするためfetchしてレスポンスを取得します。
これにより、responseのarrayBuffer()を使用できます。
さらに、arrayBuffer()によりresponseのBuffer(バイナリデータ)形式の部分を取得することができます。

ちなみに、ArrayBuffer は、バイナリデータを格納するための一般的な固定長バッファのことを指します。そのため、このメソッドは、画像やバイナリファイルなど、テキスト以外のデータを扱う際に便利です。

次に、Buffer.fromを使用してArrayBufferをBufferオブジェクトに変換します。 Bufferオブジェクトに変換することでplaiceholderbooks[0].thumbnail.url`を扱えるようしています。

あとは、plaiceholderから、base64文字列に変換した低解像度の画像を取得するだけです。

今回はわかりやすくするため、フェッチからBufferの変換を分けましたが、pliceholder公式のようにまとめて書くこともできます。
https://plaiceholder.co/docs/usage#base64

Imageコンポーネント

最後に、画像の表示ですが、blurローディングするためのpropsに対して、値を渡すだけです。
具体的には以下となります。

  • placeholder: "blur"
  • blurDataURL: base64

これを設定するだけで、blurローディングができます。

import Image from 'next/image'
import { getPlaiceholder } from 'plaiceholder'

const Home = async () => {
  // ※ 関連する処理のみ抜粋のため、import文含め省略しています

  const response = await fetch(books[0].thumbnail.url)
  const arrayBuffer = await response.arrayBuffer()
  const buffer = Buffer.from(arrayBuffer)
  const { base64 } = await getPlaiceholder(buffer)

  return (
    <main className={base()}>
      <Image
        src="/default_icon.png"
        width={200}
        height={200}
        priority={true}
        alt="test"
      />
      <Image
        src={books[0].thumbnail.url}
        width={200}
        height={200}
        placeholder="blur"
        blurDataURL={base64}
        alt="test"
      />
    </main>
  )
}

デモ

以下がデモになります。
※ ご自身で実装して検証する際はDeveloper Toolのネットワークで「キャッシュを無効」にチェックをし、「高速3G」または「低速3G」で実装の確認をすることを推奨します

demo

参考文献

https://nextjs.org/docs/pages/api-reference/components/image
https://zenn.dev/nameless_sn/articles/nextjs_image_important_points
https://ja.legacy.reactjs.org/docs/code-splitting.html#reactlazy
https://twitter.com/acdlite/status/1758229889595977824
https://zenn.dev/ikenohi/scraps/907d8b909a0824
https://plaiceholder.co/docs

Discussion