結論:Webフォントの遅さは「リクエストの連鎖」、ガタつきは「フォールバックとの寸法差」。原因が別なので対策も別に打つ
- 遅さの正体=CSS取得→フォント本体取得の2段待ちと別ドメイン接続。preconnectを2行(gstaticはcrossorigin必須)で先回りさせる
- font-displayは無指定だと最大3秒の見えない時間。本文はswap、装飾用途はoptionalが実務の既定
- swapにするとガタつき(CLS)が出る。size-adjust付きのフォールバック@font-faceで寸法を合わせれば消える
- 日本語フォントは桁違いに重い。ウェイトを2つまでに削る方が小手先の最適化より効く
- 直したつもりで直っていない事故の大半は、preconnectのcrossorigin忘れとキャッシュ有効のまま計測している2つ
サイトにWebフォントを入れた途端に表示が重くなった、あるいは開いた直後に文字が一瞬別の書体で出てからガタッと入れ替わる——Web制作の現場でいちばん相談が多い表示速度トラブルです。画像の遅延読み込みは対策が知られてきましたが、フォントはLCP(最大要素の表示)とCLS(レイアウトのずれ)の両方を同時に悪化させるため、影響が読みにくい。
結論から言うと、この2つの症状は原因が別物です。遅いのはリクエストの連鎖、ガタつくのはフォールバックとの寸法差。ここを混ぜたまま「font-display: swap を付けたのに直らない」と悩んでいるケースが非常に多いので、順番に切り分けていきます。
なぜWebフォントを入れるとLCPが遅くなるのか
Google Fontsを推奨どおりの<link>タグで読み込むと、ブラウザの中では次の順番で待ちが発生します。
- 1段目:
fonts.googleapis.comへ接続してCSSを取得する(DNS→TCP→TLSの3工程) - 2段目:そのCSSの中身を読んで初めて
fonts.gstatic.comにフォント本体(woff2)があると分かる - 3段目:別ドメインなので接続を最初からやり直し、woff2を取得する
問題は2段目です。フォント本体のURLはCSSを読むまで分からないので、ブラウザは先読みができません。接続の確立を2回、直列で待つことになります。国内の光回線でも1回あたり数十ミリ秒、モバイル回線や海外からのアクセスでは1回で100ms以上かかることもあり、これが丸ごとLCPに上乗せされます。
そして本文がWebフォント待ちになっている間、ブラウザは既定で最大3秒間、文字をまったく描画しません(FOIT=不可視テキスト)。ファーストビューの見出しがLCP要素になっているサイトでは、この待ち時間がそのままLCPのスコアになります。画像のLCPはloading=”lazy”を全画像に付けるとLCPが悪化する記事で扱いましたが、フォントの場合は「読み込む要素が重い」のではなく「見つけるのが遅い」のが本質で、打つ手も変わります。
まず計測する:どこで待っているかを確定させる
対策を入れる前に、必ず現状を測ります。順番を飛ばすと「効いたつもり」で終わります。
- DevTools → Network → フィルタに
fontを入れる。Waterfall欄の色の内訳を見る(薄い部分=接続の確立待ち、濃い部分=実際のダウンロード) - 接続待ちが長いならpreconnectの問題、ダウンロードが長いならファイルサイズの問題。打つ手が完全に分かれる
- Networkパネル上部の「Disable cache」に必ずチェックを入れる。2回目以降はキャッシュから返るため、入れ忘れると全部が速く見えて何も判断できない
- Performanceパネルで記録し、Layout Shiftのマーカーがフォント取得完了の直後に立っていないかを見る。立っていればガタつきはフォント起因で確定
この4点だけで、自分のサイトが「遅い」のか「ガタつく」のか、あるいは両方なのかがはっきりします。
対策1:preconnectを2行入れる(crossorigin必須)
接続の確立を先回りさせて、CSS取得と並行させます。<head>のできるだけ上、フォントの<link>より前に置きます。
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap">
ここでいちばん踏まれる落とし穴が、2行目の crossorigin 属性です。フォントファイルはCORSモードで取得される仕様なので、crossorigin なしでpreconnectした接続は別物の接続として扱われ、まったく再利用されません。つまり1行目だけ効いて2行目は無駄になり、いちばん効かせたかったgstatic側の接続待ちがそのまま残ります。しかもDevToolsのNetworkパネルで見ても「preconnectのタグはある」ので、直っていないことに気づきにくい。
効いたかどうかは、フォントのWaterfallから接続待ちの区間が消えているかで判定します。消えていなければcrossorigin忘れをまず疑ってください。
なおpreconnectは、書けば書くほど速くなるものではありません。接続の確保にはクライアント側のリソースを使うので、実際に使うドメインだけ、3〜4本までに留めます。使わないドメインへのpreconnectはむしろ有害です。
対策2:font-displayを用途で選び分ける
Google Fontsの場合はURLの末尾に &display=swap を付けるだけ、セルフホストなら@font-faceにfont-displayを書きます。値の意味は次のとおりです。
auto(無指定):最大3秒間、文字が見えない。これが既定なので、何も書いていないサイトはこの状態swap:すぐ代替フォントで描画し、取得できたら差し替える。FOITは消えるがガタつき(FOUT)が出るfallback:見えない時間は約100msだけ。その後は代替で描画し、約3秒以内に間に合えば差し替えるoptional:見えない時間は約100ms。間に合わなければ今回は差し替えない(次回以降キャッシュから使う)。差し替えが起きないのでCLSはゼロ
実務での選び分けはシンプルです。読ませる本文や見出しは swap(文字が見えないより、書体が後から変わるほうがマシ)。ロゴ周りや装飾的なあしらいは optional(初回で差し替わらなくても実害がなく、CLSを確実にゼロにできる)。アイコンフォントもoptionalが無難です。
対策3:swapで出るガタつき(CLS)をsize-adjustで消す
swapにすると今度はガタつきが目立ちます。これは代替フォントとWebフォントで1文字あたりの幅と、ベースラインの位置が違うために起きます。行数が変わるほど差があると、下の要素まで一斉に動いてCLSが跳ね上がります。
対策は「代替フォント側の寸法を、Webフォントに合わせて補正する」ことです。ローカルフォントを@font-faceで包み直し、size-adjustとascent-overrideで寸法を寄せます。
/* 差し替え前に使われる代替フォントを、寸法補正して定義する */
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%; /* 1文字の幅を補正 */
ascent-override: 90%; /* 行の高さ・ベースライン位置を補正 */
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: "Inter", "Inter Fallback", sans-serif;
}
ポイントはfont-familyのフォールバック指定に、素のArialではなく補正済みの"Inter Fallback"を書くことです。ここを元のままにすると補正した@font-faceは一度も使われず、当然CLSも変わりません。定義しただけで満足してしまう事故が起きやすい箇所です。
数値は書体ごとに違うので、1回で合わせようとせず、5%刻みで動かしてPerformanceパネルのLayout Shiftが消える点を探すのが早い。完全一致は不要で、行数が変わらないところまで寄せれば体感上のガタつきはほぼ消えます。なお、このやり方は「はみ出し」系の崩れとも相性があり、幅がシビアな1行レイアウトではmin-width:autoによる縮まない問題と同時に出ることがあります。フォント差し替えで文字幅が増えた瞬間にはみ出す、という組み合わせです。
対策4:そもそも読み込む量を削る(日本語フォントは別格)
ここまでは接続の最適化ですが、ダウンロード時間が長い場合は小手先では効きません。削るのが唯一の解です。
- ウェイトは2つまで。400と700だけにする。
wght@300;400;500;600;700のような指定は、1書体で5ファイル分を取りに行くのと同じ - イタリックは本当に使うときだけ。使っていないのに指定だけ残っている例が非常に多い
- 日本語フォントは桁が違う。欧文が1ウェイト数十KB程度なのに対し、日本語は収録字数が万単位なので圧倒的に重い。Google Fontsは日本語を細かく分割配信してくれますが、それでも欧文と同じ感覚で複数ウェイトを並べると一気に重くなる
- 日本語で見出しだけに使うなら、使う文字だけを切り出すサブセット化が最も効く。ロゴやキャッチコピーのように文字が固定なら効果は絶大
実務の感覚として、ウェイトを5つから2つに減らす一手が、preconnectやsize-adjustの合計よりも効くことは珍しくありません。順番としては「まず削る、次に接続を最適化する、最後に寸法を合わせる」が正しい。
セルフホストに切り替えるべきか
woff2を自サーバーに置く選択肢もあります。判断基準はこうです。
- セルフホストが有利:サイト本体と同じドメインに置けるので、接続の確立が1回で済む。
<link rel="preload">でHTMLの時点から先読みでき、対策1の連鎖そのものが消える。ライセンス上の配布可否は要確認 - Google Fontsのままが無難:更新の追従やサブセット配信を任せられる。WordPressのテーマ側が読み込んでいる場合、テーマ更新で書き戻されるため、子テーマ側での差し替え運用が前提になる
WordPressで作業する場合、テーマやプラグインがwp_enqueue_styleで勝手にGoogle Fontsを読んでいることがよくあります。自分で書いた覚えがないのにフォントのリクエストがあるなら、まずそこを疑ってください。子テーマのfunctions.phpで該当ハンドルをwp_dequeue_styleしてから、自前の読み込みに差し替えるのが定石です。
実際に踏んだ落とし穴
- preconnectの
crossorigin忘れ:タグは正しく2行入っているのにWaterfallの接続待ちが消えない。原因はこれ一択だった。2行目だけに付けるのが正で、1行目のCSS側に付けても意味はない - キャッシュ有効のまま計測していた:修正前後で差が出ずに悩んだが、Disable cacheを入れ忘れていただけ。フォントの計測はシークレットウィンドウ+Disable cacheを固定の手順にすると事故らない
size-adjustを定義しただけでfont-familyに入れていなかった:CLSがまったく改善せず、原因の特定に時間を取られた。定義と適用は別物- 使っていないウェイトが残っていた:デザイン検討時にLightを試して、採用しなかったのにURLの指定だけ残存。実際に使われているウェイトは、DevToolsのElements → Computed →
font-weightで当たりを付けられる - アイコンフォントが
swapだった:差し替わる瞬間に、代替フォントで表示されていた意味不明な文字がアイコンに変わって見えた。アイコン用途はoptionalか、そもそもSVGに置き換えるのが正解
まとめ
Webフォントの表示問題は、「遅い(LCP)」と「ガタつく(CLS)」を分けて考えるだけで一気に見通しが良くなります。遅さはリクエストの連鎖が原因なので、preconnect2行(gstatic側はcrossorigin必須)と、読み込むウェイトを削ることで対処する。ガタつきは差し替え時の寸法差が原因なので、size-adjust付きのフォールバック@font-faceを定義し、それをfont-familyに実際に書いて対処する。用途別にswapとoptionalを選び分ければ、そもそもガタつきを発生させない箇所も作れます。
そして計測は必ずキャッシュ無効で行う。これを手順として固定しておけば、「直したのに変わらない」という一番消耗する時間をまるごと回避できます。表示速度は数値を追う作業に見えて、実際はどこで待っているかを正しく見るだけの作業です。
関連記事(コーディング実務Tips)
- loading=”lazy”を全画像に付けるとLCPが悪化する|画像の遅延読み込みの使い分け
- flexアイテムが縮まない・ellipsisが効かない原因はmin-width:auto
- スマホで横スクロールが出る原因の特定手順と直し方
- object-fitとaspect-ratioが効かない・画像の縦横比が崩れる原因と直し方
- 文字の上下にできる余白の正体はhalf-leading
- 背景画像が表示されない原因はurl()の基準と箱の有無
- inputやselectのCSSが効かない原因はappearance
多言語・ベトナム語サイトの制作でお困りではありませんか?
フォント・文字化け・WordPress多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。
多言語サイト制作の詳細を見る →すぐに相談したい方は お問い合わせフォームへ