Cover Image for next/image を利用した画像最適化で CWV の数値を改善する

next/image を利用した画像最適化で CWV の数値を改善する

概要

imgurにあげている画像を参照しているが、最適に表示できていなかったのでCWVの数値が悪い。next/imageの機能を使って最適化してみる。改善ポイントは以下

  • next/image を使った画像の最適化
    • 画像サイズの最適化
    • preload と lazyload の使い分け

改善前後の変化

改善前

改善後

画像の最適化

webpを使って軽量な画像にしてたつもりだったが、LCPやCLSの値が悪い。 next/image の CustomLoader を作って imgur の画像を最適化することで解決する

変更前のコード(抜粋)

srcSet を使って最適な画像を使うように設定してるが、Layout Shift が起きる next/image の CustomLoader を使えば良さそうなことはわかっていたが、やり方がわからなかったので一旦放置していた。

// cover-image.tsx
import cx from 'classnames'
import Link from 'next/link'

type Props = {
  title: string
  src: string
  slug?: string
}

const CoverImage = ({ title, src, slug }: Props) => {
  const srcSet = [
    `${src}h.webp 1024w`,
    `${src}l.webp 640w`,
    `${src}m.webp 320w`
  ]
  const image = (
    <picture>
      <source
        type="image/webp"
        className="max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl 2xl:max-w-2xl"
        srcSet={srcSet.join(',')}
      />
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img
        src={`${src}l.jpeg`}
        alt={`Cover Image for ${title}`}
        loading="lazy"
        className={cx('shadow-sm', {
          'hover:shadow-lg transition-shadow duration-200': slug
        })}
      />
    </picture>
  )
  return (
    <div className="sm:mx-0">
      {slug ? (
        <Link as={`/posts/${slug}`} href="/posts/[slug]">
          <a aria-label={title}>{image}</a>
        </Link>
      ) : (
        image
      )}
    </div>
  )
}

export default CoverImage

変更後のコード(抜粋)

next/image を使うように修正した。CustomLoaderのコードは後述する。 画像の高さを固定することにより、Layout Shift が起きないようになっている。 これでCLSの数値を改善できる。

また、Hero Image として画像を使う場合は preload するように設定した。これでLCPも改善できる。( PageSpeed Insights のアドバイス通りにやっただけなので特に難しいことはない )

// cover-image.tsx
import cx from 'classnames'    
import Image from 'next/image'  
import Link from 'next/link'  
  
import { customLoader } from '../lib/image-loader'  
  
type Props = {  
  title: string  
  src: string  
  slug?: string  
  isHero?: boolean  
}  
  
const CoverImage = ({ title, src, slug, isHero }: Props) => {  
  // HeroImageとして使うかそうでないかでサイズを分ける
  const height = isHero ? 'clamp(200px,50vw,1000px)' : 'clamp(200px,30vw,500px)'  
  const image = (  
    <figure  
      style={{ position: 'relative', height: `${height}` }}  
      className={cx('relative shadow-sm w-full', {  
        'hover:shadow-lg transition-shadow duration-200': slug  
      })}  
    >  
      <Image
        loader={customLoader}  
        src={src}  
        alt={`Cover Image for ${title}`}  
        layout="fill"  
        objectFit="contain"  
        priority={isHero} // hero image のときは preload する  
      />  
    </figure>  )  
  return (  
    <div className="sm:mx-0">  
      {slug ? (  
        <Link as={`/posts/${slug}`} href="/posts/[slug]">  
          <a aria-label={title}>{image}</a>  
        </Link>      ) : (  
        image  
      )}  
    </div>  
  )  
}  
  
export default CoverImage

上で使っている CustomLoader 最低限の実装だけしている。 imgurの画像でない場合はそのまま使い、imgurの画像の場合は表示幅に合わせて適切な大きさの thumbnail が表示されるURLを生成するようになっている

// image-loader.ts
import { ImageLoader } from 'next/dist/client/image'  
  
export const customLoader: ImageLoader = ({ src, width, quality }) => {  
  if (!src.includes('https://i.imgur.com')) return src  
  if (width >= 1024) return `${src}h.webp`  
  if (width >= 640) return `${src}l.webp`  
  return `${src}m.webp`  
}