ウェブフォントとパフォーマンスの両立を諦めない

2024/11/20に公開

ウェブページのパフォーマンス向上を目指す場合、ウェブフォント(特に和文フォント)の使用は避けるべきと言われます。これは、ウェブフォントのファイルサイズが大きく、ページの読み込み速度に大きな影響を与えるためです。

しかし、デザイン上の要件やブランディングの観点から、ウェブフォントの使用が必要不可欠なケースもあるでしょう。本記事では、ウェブフォントを使用しながら、できる限りパフォーマンスを最適化するためのヒントをご紹介します。

検証環境

  • 使用フォント: Google Fonts の Noto Sans JP
  • 開発環境: Next.js
  • 測定ツール: Lighthouse
    • 説明の都合上、First Input Delay (FID) など、古い指標についても言及します

和文フォントが重いのはなぜか

欧文フォントと和文フォントには、収録文字数に違いがあります。欧文フォントはラテン文字と記号で約 100 文字程度であるのに対し、和文フォントはひらがな、カタカナ、漢字、記号を含め数千文字にも及びます。

このような収録文字数の違いに加え、文字の構造自体にも大きな差異があります。欧文は比較的単純な線や曲線で構成されているのに対し、漢字は複雑な字形構造を持っています。そのため一文字あたりのサイズも和文フォントの方が大きくなりがちです。

これらの要因により、欧文フォントが数十 KB 程度で済むのに対し、和文フォントは数 MB、場合によっては数十 MB にも及ぶファイルサイズとなります。

Lighthouse のモバイル環境テストでは、Slow 4G 回線を想定したエミュレーションが行われます。この条件下では、数 MB の和文フォントファイルのダウンロードに数十秒を要することがあります。

unicode-range によるウェブフォントの分割配信

Google フォントは、効率的なフォント配信のためにフォントファイルをサブセットに分割して提供しています。CSS の unicode-range プロパティを使用して、ページで実際に使用している文字のみを選択的にダウンロードすることが可能です。

https://developer.mozilla.org/ja/docs/Web/CSS/@font-face/unicode-range

download_font
分割したフォントファイルがダウンロードされている

unicode-range は CSS の @font-face ルールに指定できるプロパティで、フォントファイルの適用範囲を Unicode 符号位置で指定します。

例えば、unicode-range: U+30-39 と設定すると、アラビア数字(0-9)に対応するフォントのみがダウンロードされます。

Noto Sans JP の CSS ファイルは、以下のように unicode-range で特定の文字範囲を指定しています。

/* https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap */

/* [0] */
@font-face {
  font-family: "Noto Sans JP";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosansjp/v53/-F62fjtqLzI2JPCgQBnw7HFowwII2lcnk-AFfrgQrvWXpdFg3KXxAMsKMbdN.0.woff2)
    format("woff2");
  unicode-range: U+25ee8, U+25f23, U+25f5c, U+25fd4, U+25fe0, U+25ffb, U+2600c,
    U+26017, U+26060, U+260ed, U+26222, U+2626a, U+26270, U+26286, U+2634c,
    U+26402, U+2667e, U+266b0, U+2671d, U+268dd, U+268ea, U+26951, U+2696f,
    U+26999, U+269dd, U+26a1e, U+26a58, U+26a8c, U+26ab7, U+26aff, U+26c29,
    U+26c73, U+26c9e, U+26cdd, U+26e40, U+26e65, U+26f94, U+26ff6-26ff8,
    U+270f4, U+2710d, U+27139, U+273da-273db, U+273fe, U+27410, U+27449,
    U+27614-27615, U+27631, U+27684, U+27693, U+2770e, U+27723, U+27752,
    U+278b2, U+27985, U+279b4, U+27a84, U+27bb3, U+27bbe, U+27bc7, U+27c3c,
    U+27cb8, U+27d73, U+27da0, U+27e10, U+27eaf, U+27fb7, U+2808a, U+280bb,
    U+28277, U+28282, U+282f3, U+283cd, U+2840c, U+28455, U+284dc, U+2856b,
    U+285c8-285c9, U+286d7, U+286fa, U+28946, U+28949, U+2896b, U+28987-28988,
    U+289ba-289bb, U+28a1e, U+28a29, U+28a43, U+28a71, U+28a99, U+28acd,
    U+28add, U+28ae4, U+28bc1, U+28bef, U+28cdd, U+28d10, U+28d71, U+28dfb,
    U+28e0f, U+28e17, U+28e1f, U+28e36, U+28e89, U+28eeb, U+28ef6, U+28f32,
    U+28ff8, U+292a0, U+292b1, U+29490, U+295cf, U+2967f, U+296f0, U+29719,
    U+29750, U+29810, U+298c6, U+29a72, U+29d4b, U+29ddb, U+29e15, U+29e3d,
    U+29e49, U+29e8a, U+29ec4, U+29edb, U+29ee9, U+29fce, U+29fd7, U+2a01a,
    U+2a02f, U+2a082, U+2a0f9, U+2a190, U+2a2b2, U+2a38c, U+2a437, U+2a5f1,
    U+2a602, U+2a61a, U+2a6b2, U+2a9e6, U+2b746, U+2b751, U+2b753, U+2b75a,
    U+2b75c, U+2b765, U+2b776-2b777, U+2b77c, U+2b782, U+2b789, U+2b78b,
    U+2b78e, U+2b794, U+2b7ac, U+2b7af, U+2b7bd, U+2b7c9, U+2b7cf, U+2b7d2,
    U+2b7d8, U+2b7f0, U+2b80d, U+2b817, U+2b81a, U+2d544, U+2e278, U+2e569,
    U+2e6ea, U+2f804, U+2f80f, U+2f815, U+2f818, U+2f81a, U+2f822, U+2f828,
    U+2f82c, U+2f833, U+2f83f, U+2f846, U+2f852, U+2f862, U+2f86d, U+2f873,
    U+2f877, U+2f884, U+2f899-2f89a, U+2f8a6, U+2f8ac, U+2f8b2, U+2f8b6,
    U+2f8d3, U+2f8db-2f8dc, U+2f8e1, U+2f8e5, U+2f8ea, U+2f8ed, U+2f8fc,
    U+2f903, U+2f90b, U+2f90f, U+2f91a, U+2f920-2f921, U+2f945, U+2f947,
    U+2f96c, U+2f995, U+2f9d0, U+2f9de-2f9df, U+2f9f4;
}

/* [1] */
@font-face {
  font-family: "Noto Sans JP";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosansjp/v53/-F62fjtqLzI2JPCgQBnw7HFowwII2lcnk-AFfrgQrvWXpdFg3KXxAMsKMbdN.1.woff2)
    format("woff2");
  unicode-range: U+1f235-1f23b, U+1f240-1f248, U+1f250-1f251, U+2000b,
    U+20089-2008a, U+200a2, U+200a4, U+200b0, U+200f5, U+20158, U+201a2,
    U+20213, U+2032b, U+20371, U+20381, U+203f9, U+2044a, U+20509, U+2053f,
    U+205b1, U+205d6, U+20611, U+20628, U+206ec, U+2074f, U+207c8, U+20807,
    U+2083a, U+208b9, U+2090e, U+2097c, U+20984, U+2099d, U+20a64, U+20ad3,
    U+20b1d, U+20b9f, U+20bb7, U+20d45, U+20d58, U+20de1, U+20e64, U+20e6d,
    U+20e95, U+20f5f, U+21201, U+2123d, U+21255, U+21274, U+2127b, U+212d7,
    U+212e4, U+212fd, U+2131b, U+21336, U+21344, U+213c4, U+2146d-2146e,
    U+215d7, U+21647, U+216b4, U+21706, U+21742, U+218bd, U+219c3, U+21a1a,
    U+21c56, U+21d2d, U+21d45, U+21d62, U+21d78, U+21d92, U+21d9c, U+21da1,
    U+21db7, U+21de0, U+21e33-21e34, U+21f1e, U+21f76, U+21ffa, U+2217b,
    U+22218, U+2231e, U+223ad, U+22609, U+226f3, U+2285b, U+228ab, U+2298f,
    U+22ab8, U+22b46, U+22b4f-22b50, U+22ba6, U+22c1d, U+22c24, U+22de1,
    U+22e42, U+22feb, U+231b6, U+231c3-231c4, U+231f5, U+23372, U+233cc,
    U+233d0, U+233d2-233d3, U+233d5, U+233da, U+233df, U+233e4, U+233fe,
    U+2344a-2344b, U+23451, U+23465, U+234e4, U+2355a, U+23594, U+235c4,
    U+23638-2363a, U+23647, U+2370c, U+2371c, U+2373f, U+23763-23764, U+237e7,
    U+237f1, U+237ff, U+23824, U+2383d, U+23a98, U+23c7f, U+23cbe, U+23cfe,
    U+23d00, U+23d0e, U+23d40, U+23dd3, U+23df9-23dfa, U+23f7e, U+2404b,
    U+24096, U+24103, U+241c6, U+241fe, U+242ee, U+243bc, U+243d0, U+24629,
    U+246a5, U+247f1, U+24896, U+248e9, U+24a4d, U+24b56, U+24b6f, U+24c16,
    U+24d14, U+24e04, U+24e0e, U+24e37, U+24e6a, U+24e8b, U+24ff2, U+2504a,
    U+25055, U+25122, U+251a9, U+251cd, U+251e5, U+2521e, U+2524c, U+2542e,
    U+2548e, U+254d9, U+2550e, U+255a7, U+2567f, U+25771, U+257a9, U+257b4,
    U+25874, U+259c4, U+259cc, U+259d4, U+25ad7, U+25ae3-25ae4, U+25af1,
    U+25bb2, U+25c4b, U+25c64, U+25da1, U+25e2e, U+25e56, U+25e62, U+25e65,
    U+25ec2, U+25ed8;
}

/* [2] */
@font-face {
  font-family: "Noto Sans JP";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosansjp/v53/-F62fjtqLzI2JPCgQBnw7HFowwII2lcnk-AFfrgQrvWXpdFg3KXxAMsKMbdN.2.woff2)
    format("woff2");
  unicode-range: U+ffd7, U+ffda-ffdc, U+ffe0-ffe2, U+ffe4, U+ffe6, U+ffe8-ffee,
    U+1f100-1f10c, U+1f110-1f16c, U+1f170-1f1ac, U+1f200-1f202, U+1f210-1f234;
}

/* ... */

このような仕組みにより、Google フォントは必要な文字セットのみを含むフォントファイルを効率的に配信し、ページの読み込み時間とリソース使用量を最適化しています。

しかし、それでもなお、常用漢字でフォントファイルのサイズは合計数 MB に及びます。

フォント分割がもたらす CSS サイズへの影響

unicode-range を使用することで、フォントファイルの分割配信は可能ですが、トレードオフとして CSS ファイルのサイズが増加します。

例えば、Noto Sans JP の CSS ファイルは、圧縮前で約 120kB、gzip 圧縮後でも約 30kB になります。

30kB は一見して大きなサイズではありませんが、ページの初期ロード時にはすべてのリソースが帯域を奪い合うため、実際の影響は単純なファイルサイズ以上になることがあります。

CSS ファイルのダウンロード優先順位は Highest priority ではありますが、Lighthouse のモバイル環境における Slow 4G 回線エミュレーションでは、ダウンロードに数秒かかることもあります。

ブラウザは CSS の読み込みが完了するまでレンダリングを待機するため、CSS ファイルが大きいと First Contentful Paint (FCP) の遅延につながります。

FCP は CSS のロードが完了するまで遅延する
FCP は CSS のロードが完了するまで遅延する

ウェブフォントの優先ダウンロードによる他リソースへの影響

ウェブフォントは Highest priority でダウンロードされます。そのため、フォントファイルが大きい場合、限られた帯域幅の中で他のリソースのダウンロードが後回しになる可能性があります。

この影響は特に Lighthouse のモバイル環境で顕著になり、JavaScript ファイルのダウンロードの遅延を引き起こすことがあります。その結果、First Input Delay (FID) の悪化につながる可能性があります。

ウェブフォントの最適化

ウェブフォントの使用とパフォーマンスの両立を図るためのアプローチをご紹介します。

Brotli 圧縮による CSS ファイルの削減

Brotli は gzip よりも高い圧縮率を実現できる圧縮アルゴリズムです。一般的なファイルでは gzip との差は数パーセント程度ですが、Noto Sans JP の CSS ファイルでは顕著な効果が見られます。具体的には、圧縮前の約 120kB から、gzip での約 30kB に対し、Brotli では約 18kB まで圧縮することができます。

この高い圧縮率が実現できる理由は、@font-face 宣言を含む CSS ファイルの特徴にあります。このファイルには同様の記述パターンが繰り返し出現するため、辞書ベースの圧縮を採用する Brotli のアルゴリズムと相性が良く、効率的な圧縮が可能です。結果、CSS ファイルサイズが削減され、First Contentful Paint (FCP) の改善が期待できます。

ただし、Google Fonts の CDN は gzip 圧縮のみに対応しているため、Brotli 圧縮を利用するには、フォントの CSS ファイルを自前で配信する必要があります。

https://ja.wikipedia.org/wiki/Brotli

content-visibility を利用したフォントの遅延ロード

unicode-range を使用することで、ページで必要な文字のみをダウンロードできますが、これらは即時ロードされます。画像の遅延ロードのように、テキストがビューポートに近づくまでフォントのロードを遅延できれば、初期ロード量を削減できます。

この課題に対しては、content-visibility が有効です。これはブラウザに要素のレンダリングを遅延する指示を与える CSS プロパティで、content-visibility: auto を指定することで、要素が必要になるまでレイアウトとレンダリングの処理を省略できます。

content-visibility の特徴:

  • 必要になった時点でレンダリングを再開
  • ページ内検索やタブ順序ナビゲーションなどのユーザーエージェント機能は通常通り利用可能
  • フォーカスや選択も通常通り機能

このプロパティは主にレンダリングの計算量削減のために利用されますが、レンダリングがスキップされた要素のフォントダウンロードも遅延されます。これにより、要素がビューポートに近づくまでフォントのダウンロードを遅延させ、初期ロード量を削減することができます。

https://developer.mozilla.org/ja/docs/Web/CSS/content-visibility

以下は、常用漢字のフォントを content-visibility プロパティを使用して遅延ロードする様子です。スクロール操作に合わせて、フォントファイルが読み込まれていく様子を確認できます。

https://youtu.be/TqTFD7tr-ys

Google Fonts とキャッシュパーティショニング

Google Fonts は CDN を通じてフォントファイルを提供しており、多くのウェブサイトで同じフォントファイルが利用されています。以前は異なるサイト間でもブラウザキャッシュを共有できていましたが、現在はキャッシュパーティショニングの導入により、サイト間でのキャッシュ共有が制限されています。

ただし、同一ドメイン内に複数のアプリケーションが存在する場合は、それらの間でキャッシュを共有することが可能です。そのため、サイトのアーキテクチャによっては、現在でもキャッシュ共有のメリットを享受できる可能性があります。

https://developer.chrome.com/blog/http-cache-partitioning?hl=ja

CJK フォントと Next.js

Next.js はフォントの最適化機能を提供していますが、デフォルト設定は CJK(Chinese, Japanese, Korean)フォントには必ずしも適していません。これは、CJK 言語が他の言語と比べて文字数が圧倒的に多く、フォントファイルのサイズが大きくなる特徴があるためです。

特に注意が必要なのは、デフォルトで有効になっている preload 機能です。和文フォントのような大きなフォントファイルを preload すると、初期ロード時のネットワーク帯域の大部分がフォントのダウンロードに使用され、他のリソースの読み込みに影響を与える可能性があります。そのため、CJK フォントを使用する場合は preload 機能を無効にすることをおすすめします。

https://nextjs.org/docs/pages/building-your-application/optimizing/fonts

まとめ

この記事がウェブフォントとパフォーマンスを両立するための一助になれば幸いです。

IVRy では一緒に働いてくれるエンジニアを全方位で絶賛募集中です。もちろんフロントエンドエンジニアも募集しています。ご連絡お待ちしています!

https://ivry.jp/
https://herp.careers/v1/ivry/FYz0GaZHff7k
https://herp.careers/v1/ivry/l3tuyPDVEL81
https://ivry-jp.notion.site/IVRy-e1d47e4a79ba4f9d8a891fc938e02271

IVRyテックブログ

Discussion