🏄

[NextJS]Core Web Vitalsを理解してNextJSで改善する機能を調べる

2024/07/29に公開

概要

Core Web Vitalsがそれぞれどういった指標であるかが曖昧で、それをNextJsの機能を使ってどのように改善できるのかが気になったので、軽く調べた際のメモになります。

Core Web Vitalsとは

Web Vitalsは、Googleが提供するウェブサイトのユーザーエクスペリエンスを評価するための一連の指標のことであり、Core Web Vitals は、その中でも特に重要なLargest Contentful Paint(LCP)、First Input Delay(FID)、Cumulative Layout Shift(CLS)からなる3つの指標のことである。

Core Web Vitalsには各指標の目標閾値が含まれていて、この閾値によって定性的にユーザーエクスペリエンスが「良好」か、「改善が必要」か、「低い」かを測定できる。
また、Googleのコア ランキング システムがランキングを決定する際に考慮する要素 であり、SEOにも大きく関わってくる。
https://developers.google.com/search/docs/appearance/core-web-vitals?hl=ja

Largest Contentful Paint(LCP)

LCPは、ユーザーがページのメインコンテンツを視覚的に認識できるまでの時間を測定する指標であり、ページロードの開始から、ビューポート内で最大のコンテンツ要素がレンダリングされるまでの時間を指している。
目標値は2.5秒以下とされている。

LCPの測定対象(メインコンテンツ)となる要素には、画像要素 (<img>タグ内の画像、url()関数を使用した背景画像)、ビデオ要素 (<video>)、ブロックレベルのテキスト要素(<div>,<p>,<h1>,など)などがあり、不透明度が0で、ユーザーに表示されない要素や背景とみなされるビューポート全体を覆う要素などは対象の要素から除外される

LCPが報告されるタイミング

ウェブページは段階的に読み込まれることが多く、ページの最大の要素が変わる可能性があるためLCPはブラウザのパフォーマンスAPIを使用して測定される。
具体的には、largest-contentful-paint型のPerformanceEntryオブジェクトを利用して、メインコンテンツが表示されたタイミングを記録しておき、ページの読み込みが進むにつれ、記録されているメインコンテンツより大きな要素が表示された場合に(ビューポート内に)、largest-contentful-paintを新規で記録するようになっている。

タイムラインにすると以下のようになる。

シナリオ1

シナリオ2

First Input Delay(FID)

FIDは、サイトのインタラクティビティや応答性に関するユーザーの第一印象を測定する指標である。
ユーザーが最初にページを操作(リンクのクリック、ボタンのタップなど)してから、その操作に応じてブラウザが実際にイベントハンドラの処理を開始するまでの時間を測定する。この遅延は、ページがまだ完全にインタラクティブでない場合に発生することが多く、目標値は100ミリ秒以下とされている。

FIDは通常、FCPTTIの間に発生する。

  • First Contentful Paint(FCP): ユーザーが最初にページに移動してから、ページのコンテンツの一部が画面に表示されるまでの時間。
  • Time to Interactive(TTI): ページが完全にインタラクティブになるまでの時間。

Webページが読み込まれる過程でCSSファイル、JSファイルのダウンロードがメインスレッドで行われるため、一時的にメインスレッドがビジー状態となる。 FIDはこういったページにコンテンツの一部がレンダリングされてはいるが、まだインタラクティブではない状態の指標である。

極端な例にはなるがコンポーネントのマウント時に重い処理を行なっている場合、FID値は高くなる傾向にある。

const Test: NextPage = () => {
  return (
    <div>
      <h1>FID Example</h1>
      <HeavyComponent />
    </div>
  )
}
const HeavyComponent = () => {
  const [clickMessage, setClickMessage] = useState('')

  const heavyTask = () => {
    let total = 0
    for (let i = 0; i < 1e9; i++) {
      total += i
    }
    return total
  }

  useEffect(() => {
    // コンポーネントのマウント時に重いタスクを実行
    heavyTask()
  }, [])

  const handleClick = () => {
    setClickMessage('ボタンが押されました')
  }

  return (
    <div>
      <button onClick={handleClick}>Click Me</button>
      {clickMessage && <p>{clickMessage}</p>}
    </div>
  )
}

Cumulative Layout Shift(CLS)

CLS は、ウェブページが読み込まれる間に発生する視覚的な不安定性を測定する指標である。例えば、ページが読み込まれる途中で、テキストが突然移動したり、ボタンが予期せず位置を変えたりする場合などの予期しないレイアウトシフトが該当する。
0.1以下が推奨値とされている。

該当するレイアウトシフト

レイアウトシフトはページが読み込まれる過程で、要素が突然移動することを指す。これは様々な形でユーザーエクスペリエンスに悪影響を及ぼす。
レイアウトシフトは既存の要素の開始位置が変更された場合、つまり二つのフレーム間で位置を変えると、レイアウトシフトが発生したとみなされるが、新しい要素の追加や既存の要素のサイズ変更が他の要素の位置に影響しない場合はレイアウトシフトとは見なされない。

  • レイアウトシフトとみなされる例

    この場合水色のボックスが、後から追加された赤色のボックスにより50vh分下に開始位置をずらしているため、レイアウトシフトが起きたと言える。

  • レイアウトシフトとみなされない例

    この場合、赤色のボックスは、既存の水色のボックスに対して影響を与えていないため、レイアウトシフトが起きたとみなされない。

レイアウトシフトのスコアの計算方法

レイアウトシフトのスコアの計測は以下の二つの要素を考慮して行われる。

  1. 影響割合
    ビューポート内の要素が移動した面積の割合を示す。

  2. 距離割合
    要素が移動した距離の割合を示します。ビューポートの高さに対する移動距離の比率。

こちらの簡単な例をでいうと

  • 最初のフレームでビューポートの半分を占める要素があり(水色のボックス)、次のフレームで、要素がビューポートの高さの25%下に移動する。つまりビューポートの75%が影響を受けているため、影響割合は0.75になる
  • 最初のフレームから水色のボックスがビューポートの高さの25%移動したため、距離の割合は0.25 になる

よってレイアウトシフトのスコアは0.75 * 0.25 = 0.1875になる。

NextJSの機能で改善する

測定

クラウドサービスを使えば、わざわざNextJSの機能を使って測定する必要はないですが、開発時に確認しながら進めたい場合やカスタムのログを送りたい場合などは、
/pages/_app.tsxまたは/app/layout.tsxuseReportWebVitalsフックを使うのがいいでしょう。

useReportWebVitals((metric) => {
    switch (metric.name) {
      //ログ送信など
      case 'LCP': {
        console.log('LCO', metric.value)
        break
      }
      case 'FID': {
        console.log('FID', metric.value)
        break
      }
      case 'CLS': {
        console.log('CLS', metric.value)
        break
      }
    }
  })

https://nextjs.org/docs/pages/api-reference/functions/use-report-web-vitals

LCPの改善

  • SSR、SSGを使う。SPAだとJSファイルのハイドレーション&レンダリングコストが大きいためSSR、SSGであらかじめHTMLを生成しておく。
  • 画像フォーマットの最適化を行う。next/imageAcceptヘッダの値を見て、ブラウザのサポートする画像フォーマットを判断し、最適な画像フォーマットに変換してくれる。
  • priority属性を付与することによって、LCPに影響する重要な画像の読み込みを優先させる。
  • デフォルトであるloading属性がついていることによって、ビューポート外の画像などのダウンロードを遅らせる。
  • next/fontを使ったフォントの最適化を行う。

FIDの改善

  • dynamic importを使う。これを使うことによりページの初期読み込みに必要なJSファイルのみをロードさせる。
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
  ssr: false,
});
  • サードパーティスクリプトを遅延読み込みすることで、ページの初期読み込みを高速化させる。
<script src="https://example.com/*******.js" async></script>
  • Web Workerを使用して、重たい処理をメインスレッドから切り離す。

CLSの改善

  • next/imageのサイズ指定(デフォルト)をして、スペースを確保しておく。
<Image
  src="/image.jpg"
  alt=""
  width={100}
  height={100}
  layout="responsive"
/>
  • 動的コンテンツのスペースを確保しておく。クライアントからAPIを呼び出し動的にコンテンツを差し込む際などは、あらかじめプレースホルダーを使用してスペースを確保しておくことにより、レイアウトシフトを防ぐ。

参考
google公式
next/image
Next.js(App Router)のnext/fontでGoogleフォントをセルフホスティング
next/image はやっぱりすごかった - 機能紹介/パフォーマンス改善例/気をつけポイント
コアウェブバイタル(Core Web Vitals)とは?LCP・FID・CLSの意味&ランキング改善方法
Core Web Vitals を改善する Next.js の機能群

Discussion