# リソースの最適化

Core Web Vitalsを準拠するためにはリソースの最適化は必要不可欠です。リソースの最適化をすることで、サイトの読み込みパフォーマンスが改善され、ユーザー体験が向上します。リソース最適化の基本的な戦略は、なるべくリソースを小さくすること、適切なタイミングで読み込むこと、キャッシュを上手に使うことです。それらを実現するため、まずはじめに、リソース全体に適用できるPRPLパターンについて見ていきます。そして、CSS、画像、フォントにおけるそれぞれの最適化を見ていきましょう。

# PRPLパターン

PRPLパターンとは、ページの読み込みとインタラクティブ性を高速化するためのパフォーマンスパターンになります。次の4つ観点からパフォーマンスの最適化を行います。

  • Preload the most important resources.
    • 最も重要なリソースを事前読み込みする
  • Render the initial route as soon as possible.
    • できるだけ早くレンダリングする
  • Pre-cache remaining assets.
    • 事前にキャッシュする
  • Lazy load other routes and non-critical assets.
    • 重要でないものは遅延読み込みする

基本的な戦略としてはリソースの読み込みと、キャッシュの最適化でパフォーマンスの向上を目指します。それらを実現するために、Preload (opens new window)Service Worker (opens new window)という技術が使用されています。

103 Early Hints

以前はHTTP/2 Server Pushという技術を使っていました。HTTP2 Server Pushとは、ページの読み込みに必要なリソースをブラウザからのリクエストを待たずに送信できるというもので、クライアントとサーバーの往復を削減するという目的で実装されました。しかし、期待したほどの改善が見込まれないこと (opens new window)やHTTP2配信のサービスのうち、1.25% (opens new window)しか使われていないという理由でChrome 106から削除されました。代替として、103 Early Hints (opens new window)という技術が実装されています。

103 Early Hintsはサーバがレスポンスを返す前に103のレスポンスを事前に返すことで、ページに必要なリソースを事前にブラウザに伝えることができます。

HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload
Link: </script.js>; rel=preload

そのため、ブラウザは受け取ったヒントをもとにサブリソースのリクエストやコネクションの準備をすることができます。メインのリソースに時間がかかっていたとしても、事前にその他のリソースを知らせることでページ全体の読み込みスピードを向上させることができます。

resource-optimization-prpl-1.avif

出典: Faster page loads using server think-time with Early Hints (opens new window)

103 Early Hintsはサーバ側での最適化ですが、同等の機能でPreloadという機能があり、こちらはクライアント側で事前に読み込みするリソースを指定することができます。

# 最も重要なリソースを事前読み込みする (Preload)

Preload (opens new window)は、リソースを事前に非同期で取得するための機能です。 通常、ブラウザは同期的に外部リソースを読み込みます。その際、リソースがダウンロード・パースされている間はレンダリングがストップします。リソースのダウンロードに時間がかかると、その分レンダリングの遅延が発生してしまいます。そのような状況を回避するためには、重要なリソースを事前に読み込んで非同期に実行する必要があります。

Preloadで取得したいリソースとしては次のようなものが考えられます。

  • ページ読み込み時に重要なリソース
  • すぐには必要ないが非同期で取得したいリソース

# ページ読み込み時に重要なリソース

Preloadは、ページ読み込み時に表示されるコンテンツに対して使うことが推奨されます。ページ読み込み時に表示されるコンテンツとは、実際にユーザーが一番はじめに見るコンテンツで、Above The Foldと呼ばれます。

resource-optimization-preload-1.avif

出典: Extract critical CSS (opens new window)

Above The Foldで必要なフォントや画像を事前に読み込むことで、ページの高速化を実現することができます。注意する点としては、全てのコンテンツをPreloadすると、意味がなくなるということです。Preloadはリソースに優先順位をつけて、最適化を図ります。そのため、全てのリソースを優先にすると、同じようなレンダリングのブロッキングが発生してしまいます。

例えば、CSS内でインポートしているフォントの場合、CSSがダウンロード・パースされてはじめてダウンロードが実行されます。そのため、フォントの適用が遅延する可能性があります。

resource-optimization-preload-0-1.avif

出典: Preload critical assets to improve loading speed (opens new window)

しかし、Preloadを使うことで、フォントファイルをCSSと並行してダウンロードすることができます。同期的にダウンロードしているときと比べるとタイムラグが減るため、フォントをスムーズに適用することができます。

<link rel=preload  as=font type=font/woff2 href=Pacifico-Bold.woff2 crossorigin>

resource-optimization-preload-0-2.avif

出典: Preload critical assets to improve loading speed (opens new window)

また、画像などでも、ページトップに表示される画像をPreloadすることで、レイアウトのがたつきなどを防ぐことができるでしょう。

<link rel=preload as=image href=image.png>

# すぐには必要ないが非同期で取得したいリソース

Preloadは非同期で実行されるため、レンダリングのブロッキングを回避することができます。そのため、JavaScriptやCSSなど、同期的に読み込むとレンダリングをブロックしてしまうようなリソースは、Preloadを使うことでパフォーマンスの最適化を図ることできます。

注意する点としては、Preloadはダウンロードするのみで実行されないということです。実行する場合は、明示的にscriptタグなどで指定する必要があります。

<link rel=preload as=script href=script.js> // ダウンロードのみ
const script = document.createElement('script');
script.src = 'script.js';
document.body.appendChild(script); // 実行する

onloadイベントを定義することでダウンロードと実行が可能になります。

<link rel=preload as=script href=script.js
      onload="const script = document.createElement('script');
              script.src = this.href;
              document.body.appendChild(script);">

CSSでも同様に非同期でダウンロードと実行をすることができます。

<link rel=preload as=style href=style.css onload="this.rel='stylesheet'">

# できるだけ早くレンダリングする (Render)

PRPLパターンでは、ページにアクセスした際の最初のレンダリングはできるだけ早く実行されることが推奨されます。画面が真っ白になり、何も表示されない状態はユーザーにとって好ましい体験ではありません。初回のページ表示速度を改善するには、SSRが有効です。

SSRは、サーバ側でレンダリングを実行するため、即座にHTMLを返すことができます。ブラウザ上ですぐにコンテンツを表示することができるため、FCPの改善が期待されます。

また、ブラウザのレンダリングのブロッキングを回避することで、初回表示の改善をすることができます。レンダリングのブロッキングを回避するには、JavaScriptの非同期読み込みやインライン化、重要なCSSのインライン化などが有効的です。

# 事前にキャッシュする (Pre-cache)

PRPLパターンでは、キャッシュの最適化を行うことで、ページ表示の改善やオフライン環境でのユーザー体験の向上を目指します。具体的には、Service Workers (opens new window)を使うことで、Preloadで事前に取得したリソースをキャッシュし、サーバへのアクセスを減らすことができます。Service Workersはバックグラウンドプロセスで動作するため、メインのプロセスをブロッキングすることはありません。そのため、ページ全体で使われる共通のリソースなどを事前にバックグラウンドで取得し、キャッシュすることができます。共通のリソースをキャッシュしておくことで、別のページへアクセスしたときでもキャッシュされたリソースを使うことができるため、ページ表示を高速化できます。

/resource-optimization-pre-cache-1.avif

出典: Apply instant loading with the PRPL pattern (opens new window)

また、Service Workersはオフライン環境でも動作するため、ネットワーク環境がない場所でもキャッシュを使ってアプリケーションを動かすことができます。そのため、ネットワークが悪い環境でのユーザー体験を最適化することができます。

# 重要でないものは遅延読み込みする (Lazy load)

PRPLパターンでは、重要なリソースはPreloadで事前に読み込むことが推奨されます。一方、重要ではないリソースは遅延読み込みすることでパーフォマンスをさらに向上させることができます。重要ではないリソースとは、ページ内にまだ現れていないコンテンツや、その他のページで使われるようなリソースを指します。そのようなコンテンツは、Below The Foldと呼ばれます。

resource-optimization-preload-1.avif

出典: Extract critical CSS (opens new window)

具体的には、JavaScriptのDynamic Importを利用してファイルを分割したり、ページに必要なファイルだけ読み込ませる必要があります。JavaScriptファイルの最適化の詳細は、JavaScriptファイルの最適化を参照してください。

その他には、画像の遅延読み込みや重要でないCSSの遅延読み込みが有効的でしょう。

# CSS

通常、ブラウザは外部CSSを同期的に読み込むため、HTMLを解析している間にCSSがダウンロード・パースされるとレンダリングがストップします。もし、CSSのダウンロードに時間がかかると、その分レンダリングが遅延します。このようなCSSをレンダリングブロッキングCSSと言います。

レンダリングブロックは、CSSの肥大化や、必要のないCSSを読み込むことで発生します。レンダリングブロックを回避するためには、以下のような施策が有効的とされます。

  • Critical CSS
  • 遅延読み込み
  • CSSファイルの圧縮・削減

# Critical CSS

Critical CSSとは、重要となるCSSをインライン化して読み込み、重要でないCSSを遅延させて読み込んでパフォーマンスを最適化させるパターンになります。重要となるCSSとは、Above The Foldで必要とされるCSSになります。重要でないCSSは、Below The Foldになります。

resource-optimization-preload-1.avif

出典: Extract critical CSS (opens new window)

例えば、次のようなCSSを考えてみましょう。

<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="./critical.css" />
    <link rel="stylesheet" href="./large.css" />
  </head>

critical.csslarge.cssという外部CSSを同期的に読み込んでいます。critical.cssAbove The Foldで必要なCSSで、large.cssが重要ではないCSSになります。この場合、large.cssの読み込みが遅れると、レンダリングブロックが発生する可能性があります。レンダリングブロックが発生することで、FCPのパフォーマンスが低下してしまいます。

これを回避するためには、次の二つの施策が有効です。

  • critical.cssをインライン化する
  • large.cssを遅延読み込みする

# critical.cssをインライン化する

重要なCSSをインライン化することで、外部CSSのダウンロードのコストを削減することができます。そのため、即座にスタイルの適用が可能となります。

<html>
  <head>
    <meta charset="UTF-8" />
    <!-- 重要なCSSをstyleタグでインライン化する-->
    <style>
      ...
    </style>
    <link rel="stylesheet" href="./large.css" />
  </head>

# large.cssを遅延読み込みする

レンダリングブロックを回避するために、large.cssを遅延読み込みします。CSSを遅延読み込みするためには、前述したPreloadを使うことができます。

<html>
  <head>
    <meta charset="UTF-8" />
    <!-- 重要なCSSをstyleタグでインライン化する-->
    <style>
      ...
    </style>
    <!-- 重要でないCSSは遅延読み込み & 実行をする-->
    <link rel="preload" href="large.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  </head>

ダウンロード・パースに時間のかかるCSSは、このように非同期で取得することで、メインのプロセスをブロックすることなく、実行できるようになります。次の例では、Critical CSS適用前と適用後の変化を見ることができます。

resource-optimization-css-1.avif

出典: Extract critical CSS (opens new window)

適用前ではレンダリングブロックの影響で画面の表示が遅れていましたが、Critical CSS適用後ではFCPが大幅に改善されていることが分かります。

注意する点としては、インライン化のCSSを増やしすぎると逆にブラウザへの負荷が高まってしまうことです。それと、インライン化されたCSSはキャッシュされないのでキャッシュ上の最適化ができなくなってしまいます。そのため、Critical CSSは大部分で使うのではなく、部分的に適用することが推奨されます。

# 遅延読み込み

Critical CSSで見たように、重要でないCSSは遅延読み込みすることでパフォーマンスを改善することができます。例えば、次のようなページを考えてみましょう。

<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="./critical.css" />
    <link rel="stylesheet" href="./no-critical.css" />
  </head>

no-critical.cssという重要でないCSSが含まれています。このページに対して、Lighthouseを実行すると以下のような改善項目が出てきます。

resource-optimization-css-defer-1.png

non-critical.cssが改善項目として検出されていることが分かります。 また、ChromeのDevToolsでCommand + Shift + Pを押下し、Coverageというツールを使うと重要なCSSとそうではないCSSを確認することができます。緑が重要で、赤が重要ではないCSSになります。

resource-optimization-css-defer-2.png

この場合、non-critical.cssは100%の割合で重要ではないCSSになります。重要ではないということは、Above the Foldでは使用されていないので遅延読み込みしても問題ないということになります。では、no-critical.cssを遅延読み込みしてみましょう。





 


<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="./critical.css" />
    <link rel="preload" href="no-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  </head>

遅延読み込みすることで非同期に処理されます。そのため、レンダリングブロックを回避することができます。もう一度、Lighthouseを実行してみると、次のような結果になります。

resource-optimization-css-defer-3.png

non-critical.cssが改善項目から無くなり、FCPが改善されていることが分かります。Critical CSSの導入が難しい場合は、必要のないCSSを抜きだし、遅延読み込みすることである程度のパフォーマンス改善が見込まれるでしょう。

# CSSファイルの圧縮・削減

リソース最適化のうち、ファイル量の削減が最も簡単で導入しやすい施策になります。近年のほとんどのバンドラーツールでは、CSSの圧縮が可能となっています。例えば、Viteではbuild.cssMinify (opens new window)を有効にすると自動でCSSの最適化をしてくれます。Webpack5では、CssMinimizerWebpackPlugin (opens new window)を使うことで最適化することができます。

# 画像

画像のおけるパフォーマンスの最適化は次のような施策が有効的です。

  • Preload
  • レスポンシブ
  • 画像CDN
  • 遅延読み込み
  • 画像の最適化

# Preload

PRPLパターンのPreloadで見たように、事前にリソースを取得することで画像の読み込みを早くすることができます。Preloadをする画像は、Above the Foldで表示されるコンテンツが望ましいでしょう。Below the Foldに表示される画像は、後述する遅延読み込みで最適化を図ることができます。Preloadをする画像は次のように書くことができます。

<link rel="preload" as="image" href="important.png">

また、Next.js13ではpriority (opens new window)オプションを設定することでPreloadの対象となります。







 


<Image
  src="/vercel.svg"
  alt="Vercel Logo"
  className={styles.vercelLogo}
  width={100}
  height={24}
  priority
/>

priorityを設定すると、次のような<link>が出力されます。Preloadが設定されると同時に、fetchpriority="high" (opens new window)を設定することで、画像の優先順位を高くしています。

<link rel="preload" as="image" href="/vercel.svg" fetchpriority="high">

Preloadを設定していないサイトを例に見てみましょう。Preloadが設定されていない場合、画像が出現してからリソースの取得を実行しているので、その分ダウンロードが遅れて発生していることが分かります。

resource-optimization-image-preload-1.png

一方、Preloadを設定していると、事前に画像の取得がされていることが分かります。

resource-optimization-image-preload-2.png

このように、Above The Fold内の画像に対して優先的にダウンロードを実行することで、画像の読み込みスピードが改善され、LCPのパフォーマンスを向上させることができます。

# レスポンシブ

レスポンシブ対応をしているサイトの場合、画像をデバイスごとに分ける必要があると思います。その場合、srcsetを設定することでデバイスごとに適した画像を取得することができます。

<img src="small.jpg" srcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w" alt="">

上記の場合、small.jpgが500pxの画像、medium.jpgが1000pxの画像、large.jpgが1500pxの画像であるとブラウザに伝えています。これらの情報を教えることで、ブラウザがデバイスに合わせて自動的に適切な画像を選定してくれます。

このレスポンシブの設定とPreloadを合わせることで、より早く、適切に画像を表示することができるようになります。Chrome73 (opens new window)からは、srcsetに記載してある画像をPreloadすることができるようになりました。

レスポンシブ画像をPreloadするためには、imagesrcsetimagesizesを使うことができます。例えば、次のような画像を考えてみましょう。

<img src="wolf.jpg" srcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" sizes="50vw" alt="...">

これをPreloadすると、<link>タグで次のように書くことができます。

<link rel="preload" as="image" href="wolf.jpg" imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" imagesizes="50vw">

imagesrcsetimagesizessrcsetsizesの値を設定すると、ブラウザにPreloadする対象を教えることができます。

# 画像CDN

画像CDNとは、画像配信に特化したCDNです。画像CDNを使うことで、オリジンリソースを変えることなく、画像のサイズ変更や最適化を行うことができます。例えば、Cloudinaryが提供しているMedia Optimizer (opens new window)では、次のように任意のサイズで画像を指定することができます。

<img src="https://mycloud.mo.cloudinary.net/rest/of/the/path.jpg?tx=w_500,h_500,c_fit" />

この例では、画像の幅を500px、高さを500pxに指定して画像のサイズを最適化しています。ランタイムで動くため、あらかじめ画像のサイズを調整する必要がありません。

画像CDNを使うことで、ユーザーに合わせて、画像を柔軟に最適化することができます。また、キャッシュされた画像はCDNサーバから配信されるため、リソースの取得速度を向上させることができます。画像CDNは、画像の最適化とキャッシュの最適化が同時に行えることが大きなメリットになります。

画像CDNの詳細はCDNの章でご確認ください。

# 遅延読み込み

画像の遅延読み込みは、以下の方法で実装することができます。

# Lazy Loading

<img>タグにloading="lazy"を設定することで、viewportから計算して一定の距離内にいる画像に対して遅延読み込みをすることができます。それ以外の画像は、読み込みが開始されないため、リソースの取得がされることはありません。

<img src="image.png" loading="lazy" alt="" width="200" height="200">

resource-optimization-image-lazy-2.avif

出典: Browser-level image lazy loading for the web (opens new window)

遅延読み込みする対象は、Below The Foldのコンテンツに対して設定すると有効的です。一方、Above The Foldに表示されるコンテンツに対してはPreloadすることで、初回ページの画像の表示を改善することができます。例えば、次のように、スクロールをして表示されるコンテンツに対しては遅延読み込みすることで、無駄なリソースの取得を抑えることができます。必要になったタイミングではじめて読み込みを開始することで、効率的なレンダリングを実現することができます。

スクロールをして、特定の距離内に達したときに読み込みが自動的に開始されます。

# Intersection Observer API

Lazy Loadingは、ほとんどのブラウザでサポート (opens new window)されていますが、polyfillとしてIntersection Observer API (opens new window)を使うことで同様の実装が可能となります。Intersection Observerは、特定の要素が指定された領域に入ったかを検知するためのAPIです。このAPIを使うことで、viewport内に画像が出現したタイミングを検知することができます。そのため、Lazy Loadingど同様にviewport内に出現したタイミングでリソースの読み込みを開始することができます。

Reactの場合、react-intersection-observer (opens new window)を使うことでIntersection Observerを実装することができます。次のように、inViewでviewport内に要素が出現したか検知することができます。

import React from 'react';
import { useInView } from 'react-intersection-observer';

const Component = () => {
  const { ref, inView, entry } = useInView({
    threshold: 0,
  });

  return (
    <div ref={ref}>
      <h2>{`Header inside viewport ${inView}.`}</h2>
    </div>
  );
};

Lazy Loadingのpolyfillとして実装する場合は、次のように書くことができます。

import React from 'react';
import useNativeLazyLoading from '@charlietango/use-native-lazy-loading';
import { useInView } from 'react-intersection-observer';

const LazyImage = ({ width, height, src, ...rest }) => {
  const supportsLazyLoading = useNativeLazyLoading();
  const { ref, inView } = useInView({
    triggerOnce: true,
    rootMargin: '200px 0px',
    skip: supportsLazyLoading !== false,
  });

  return (
    <div
      ref={ref}
      style={{
        position: 'relative',
        paddingBottom: `${(height / width) * 100}%`,
        background: '#2a4b7a',
      }}
    >
      {inView || supportsLazyLoading ? (
        <img
          {...rest}
          src={src}
          width={width}
          height={height}
          loading="lazy"
          style={{ position: 'absolute', width: '100%', height: '100%' }}
        />
      ) : null}
    </div>
  );
};

出典: React IntersectionObserver Recipes (opens new window)

# フォント

Core Web Vitalsにおいて、フォントの最適化は重要な要素になっています。フォントのダウンロードが遅延することで、FCPやLCPの低下に繋がります。また、テキストが表示されなかったり、遅延によるフォントの切り替えでテキストのサイズや段落の高さが変わったりして、CLSの悪化に繋がる可能性があります。

フォントの最適化を行う前に、フォントファイルがどのタイミングでダウンロードされるのか理解しておく必要があるでしょう。 フォントがダウンロードされるタイミングは、@font-faceで定義されたフォントがCSSのスタイルシートの中で参照されたときです。例えば、次のような@font-faceがあるとしましょう。

@font-face {
  font-family: "Open Sans";
  src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
}

この時点では、まだフォントのダウンロードは実行されません。次のようにスタイルシートの中で参照されたタイミングでフォントのダウンロードが実行されます。

@font-face {
  font-family: "Open Sans";
  src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
}

/* 参照されたタイミングでダウンロードされる */
h1 {
  font-family: "Open Sans"
}

そのため、フォントの最適化の基本的な戦略は、なるべく早くフォントのダウンロードを実行することです。スタイルシートが読み込まれたタイミングでダウンロードすると、前述したような遅延が発生する可能性があります。事前にダウンロードしておくことで、フォントの読み込みパフォーマンスを最適化することができます。最適化すべきファイルは次の2点になります。

  • @font-faceを含んだスタイルシート
  • フォントファイル(.woff2)

上記の最適化には、次のような施策が有効的です。

  • インライン化
  • Preconnect
  • Preload

一つずつ見てみましょう。

# インライン化

Critical CSSで解説したように、フォントに対してもインライン化をすることで、サーバへのリクエストをすることなく、ブラウザーで即座に読み込むことができます。@font-faceが定義されているスタイルシートを<head>タグ内に埋め込むことで、ブラウザーがすぐにフォントのダウンロードを実行することができます。

<head>
  <style>
    @font-face {
        font-family: "Open Sans";
        src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
    }
    body {
        font-family: "Open Sans";
    }
  </style>
  <link rel="stylesheet" href="https://mydomain/other.css" />
</head>

# Preconnect

preconnect (opens new window)は、事前にアクセスするサーバへの接続を確立することで、DNSの解決やTCPのハンドシェイクの最適化ができる設定です。preconnectを設定することで、リソースの取得の遅延を減らし、パフォーマンスを向上させることができます。

resource-optimization-font-preconnect-1.avif

出典: Establish network connections early to improve perceived page speed (opens new window)

フォントのダウンロード先が3rd Partyの場合、事前にpreconnectしておくことで遅延を減らすことができます。例えば、Google Fontsでは次のように設定することができます。

<head>
  <link rel="preconnect" href="https://fonts.googleapis.com"> <!-- スタイルシートのダウンロード先 -->
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <!-- フォントのダウンロード先 -->
</head>

Google Fontsは@font-faceの定義がしてあるスタイルシートとフォントを提供しているため、その二つのダウンロード先をpreconnectで設定する必要があります。注意点として、フォントのダウンロードはCORS (opens new window)を使用したリクエストにする必要があります。そのため、crossorigin属性を付与しています。

Google Fontsのサイト (opens new window)ではデフォルトで上記の設定が記載してあるので、利用者はそのままタグを貼り付ければパフォーマンスの最適化ができるようになっています。

Google Fonts

# Preload

フォントデータに対してもPreloadすることで、非同期で事前にデータを取得することができます。Google Fontsの場合、@font-faceが定義されているスタイルシートをダウンロードする必要があるため、そのurlに定義されているフォントファイルを事前にPreloadするのは難しいでしょう。(動的にurlが変わる可能性があるため)





 



@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 300;
  src: url(https://fonts.gstatic.com/s/lato/v24/S6u9w4BMUTPHh7USSwaPGQ3q5d0N7w.woff2) format('woff2');
  unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

そのため、Preloadするためにはフォントファイルを予め用意しておいてセルフ配信する必要があります。urlをセルフ配信先に置き換えて、次のように定義することができます。

  <link
    rel="preload"
    as="font"
    type="font/woff2"
    href="./fonts/lato/S6u9w4BMUTPHh7USSwaPGQ3q5d0N7w.woff2"
    crossorigin
  />

Making Google Fonts Faster in 2022 (opens new window)によると、preconnectを設定したGoogle Fontsより、セルフ配信 + Preloadのフォントの方がパフォーマンスが良くなっていることが分かります。

Google Fonts + preconnect(4.147s): google fonts + preconnect

セルフ配信 + Preload(3.388s): セルフ配信 + preload

PreconnectとPreloadどちらがいいか

前述した通り、セルフ配信 + CDN + Preloadのほうがパフォーマンス性は良くなる可能性があります。フォント定義をインライン化することでさらに読み込みを早めることもできるでしょう。しかし、Preloadを必要以上に設定すると逆にパフォーマンスが悪化する可能性があるので注意が必要です。Preloadの効果を最大化させるためには、Preloadするリソースを必要最低限に抑える必要があります。具体的な数値はサイトによって異なるため、パフォーマンスツールを使用して適宜計測することが重要となります。

# CLS

フォントのダウンロードの遅延が発生することで、フォールバックのフォントからWeb Fontに変わるタイミングでレイアウトのズレが発生する可能性があります。

resource-optimization-font-1.avif

出典: Debug layout shifts (opens new window)

レイアウトのズレを無くすためには、フォントを遅延なくダウンロードし、かつフォールバックのフォントとの差異をなるべく無くすことで実現することができます。 例えば、Next.js13では、Font Optimization (opens new window)という機能が提供されています。これは、ビルド時に@font-faceが定義されているフォントのデータをダウンロードし、Next.jsのサーバからセルフ配信することで、Google Fontsサーバへのアクセスなしでフォントを利用できる機能になります。また、フォールバックのフォントに対してsize-adjust (opens new window)を設定して、フォントによるテキストレイアウトのがたつきを抑えるように最適化されています。 さらに、スタイルシートとフォントのデータに対してPreloadを使っているので、非同期で読み込むことができます。

例えば、次のようにnext/font/googleから任意のフォントをインポートして、特定の要素に対して使用します。

import Head from 'next/head'
import Image from 'next/image'
import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'

const inter = Inter({ subsets: ['latin'] })

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={`${styles.main} ${inter.className}`}></main>

このコードをビルドすると、次のようなCSSファイルが生成されます。

@font-face {
  font-family: __Inter_0ec1f4;
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/_next/static/media/2aaf0723e720e8b9-s.p.woff2) format("woff2");
  unicode-range: U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+0304,U+0308,U+0329,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd
}

@font-face {
  font-family: __Inter_Fallback_0ec1f4;
  src: local("Arial");
  ascent-override:90.20%;
  descent-override:22.48%;
  line-gap-override:0.00%;
  size-adjust:107.40%
}

.__className_0ec1f4 {
  font-family: __Inter_0ec1f4,__Inter_Fallback_0ec1f4;
  font-style: normal
}

最初のsrcを見ると、/_next/static/media/2aaf0723e720e8b9-s.p.woff2が指定されていることが分かります。このフォントデータはビルド時に生成されたもので、.next/static/media以下に格納されています。このフォントファイルをセルフ配信することで、Google Fontsサーバにリクエストする必要がなくなります。

@font-face {
  font-family: __Inter_0ec1f4;
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/_next/static/media/2aaf0723e720e8b9-s.p.woff2) format("woff2");
  unicode-range: U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+0304,U+0308,U+0329,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd
}
.next/static/media/
├── 2aaf0723e720e8b9-s.p.woff2
├── 9c4f34569c9b36ca-s.woff2
├── ae9ae6716d4f8bf8-s.woff2
├── b1db3e28af9ef94a-s.woff2
├── b967158bc7d7a9fb-s.woff2
├── c0f5ec5bbf5913b7-s.woff2
└── d1d9458b69004127-s.woff2

そして、フォールバックとしてArialが設定されています。ascent-overridedescent-overrideline-gap-overridesize-adjustを設定することで、Google Fontのフォントとレイアウト上の差異が出ないように調整されていることが分かります。

@font-face {
  font-family: __Inter_Fallback_0ec1f4;
  src: local("Arial");
  ascent-override:90.20%;
  descent-override:22.48%;
  line-gap-override:0.00%;
  size-adjust:107.40%
}

そして、このスタイルシートとフォントファイルに対して以下のようにPreloadが設定されています。Preloadで事前に取得することで、フォントの読み込みパフォーマンスを向上させることができます。

Next.js Font Optimizationの結果

このように、フォールバックのフォントの調整とPreloadによる事前読み込みで、フォントの読み込みパフォーマンスとユーザー体験を向上させることができるようになります。