FlowingDev

フォントフォールバックの解説:CLSというレイアウトの大地震を回避する

size-adjust や ascent-override などの CSS フォントメトリクスを利用して、システムフォントをウェブフォントに完璧に一致させ、Cumulative Layout Shift を防ぐ方法を学ぼう。

ツールを試す: フォントフォールバックジェネレーター (CLS)

ひとことで言うと

フォントフォールバックメトリクスは、システムフォントを人工的に引き伸ばしたり押しつぶしたりして、カスタムウェブフォントと数学的に「完全に同じスペース」を占めるようにするCSSのスーパーパワーだ。これにより、カスタムフォントがロードされた瞬間にページレイアウトが暴力的にガタッとズレるのを防いでくれる。

どんな課題を解決するのか?

この問題を理解するには、まずウェブの歴史上で最も古いタイポグラフィ戦争について知る必要がある。つまり、「不安定な3G回線で150KBのカスタムウェブフォントのダウンロードを待っている間、ユーザーに何を見せるべきか?」という問題だ。

歴史的に見て、ブラウザの対応は2つのアプローチのどちらかだったが、どちらも最悪だった。1つ目は FOIT (Flash of Invisible Text)。フォントのダウンロードが完了するまで、ブラウザがテキストを単に隠してしまうというもの。ユーザーは真っ白な画面を見つめながら、「このサイト壊れてるのかな?」と首を傾げることになる。2つ目は FOUT (Flash of Unstyled Text)。ブラウザが即座にローカルのシステムフォント(Arial や Times New Roman など)でテキストを描画し、君のゴージャスなカスタムフォントが届いた瞬間に慌ててすり替えるというものだ。

ウェブ開発者のコミュニティでは、アクセシビリティや体感パフォーマンスの面で FOUT のほうがマシだという結論でほぼ一致した。結局のところ、ダサいテキストでも読めるなら、何も読めないよりはマシだからだ。そこで我々は、これを font-display: swap というCSSルールとして標準化した。

しかし、font-display: swap は新たなモンスターを生み出してしまった。それが Cumulative Layout Shift (CLS) だ。

すべてのフォントは独自の骨格を持っている。Roboto の "x" は、Arial の "x" よりも物理的に横幅が広い。大文字の高さも違うし、フォントファイルに組み込まれているスペースの取り方も全く異なる。ブラウザが Arial から Roboto に切り替えた瞬間、テキストが占める縦横のスペースが急に変わってしまうのだ。段落はリフローし、テキストは新しい行に折り返される。

もしユーザーがすでに記事を読んでいたり、ボタンをクリックしようとしている瞬間にこれが起こると、まるでDOMに大地震が起きたかのようにページ全体が飛び跳ねる。ユーザーのカーソルの下からボタンが逃げ出し、画像は画面の下へ押しやられる。Google はまさにこのフラストレーションを測定するために Core Web Vitals という指標を導入し、CLS が高いサイトのSEOを容赦なくペナルティの対象にした。我々には、このダサい一時的なシステムフォントに、美しいカスタムフォントの物理的な「フットプリント(足跡)」を完璧に模倣させる方法が必要だったのだ。

裏側ではどう動いているのか

レイアウトのズレを止めるために、カスタムウェブフォントをいじるわけではない。そうではなく、@font-face ルールを使ってローカルのシステムフォントの「フランケンシュタイン」バージョンを作り出すんだ。専用の CSS 記述子を使って、システムフォントの内部的なジオメトリ(形状)を上書きしてやる。

フォントボックスの解剖学

フォントを操作するには、ブラウザが1行のテキストの周囲の「ボックス」をどう計算しているかを理解する必要がある。これは単純に font-size の問題ではない。フォントファイル自体に焼き付けられている、3つの内部メトリクスに依存しているんだ。

  1. アセント (Ascent): 文字が乗っている見えない基準線(ベースライン)から、一番背の高い文字の頂点('h' や 'T' のてっぺんなど)までの距離。
  2. ディセント (Descent): ベースラインから、一番下まで垂れ下がっている文字の底('y', 'g', 'p' の尻尾など)までの距離。
  3. ラインギャップ (Line Gap): ある行のディセントと、次の行のアセントの間にある推奨される余白(よくレディング(行間)と呼ばれるもの)。

CSS において、テキストの1行の全体的な高さは、ざっくり アセント + ディセント + ラインギャップ になる。もし君のカスタムフォントとシステムフォントでこれら3つのメトリクスに違いがあれば、レイアウトは縦にジャンプする。文字の横幅が違えば、レイアウトは横にジャンプするというわけだ。

そこで登場するのがオーバーライド

CSS Fonts Module Level 4 は、@font-face ブロック内で使える4つの魔法のような記述子を導入した。これらの記述子は、ブラウザがフォントファイルの内部メトリクスを読み込むのを途中で傍受し、その場で上書きしてしまうんだ。

  • size-adjust: これはフォント全体に対する掛け算のような働きをする。もしカスタムフォントが全体的に Arial より広いなら、Arial に size-adjust: 110% を設定して横に引き伸ばすことができる。
  • ascent-override: フォント内部のアセントメトリクスを上書きする。
  • descent-override: フォント内部のディセントメトリクスを上書きする。
  • line-gap-override: フォント内部のラインギャップメトリクスを上書きする。

計算を組み合わせる

ここでのトリックは、size-adjust は横幅、アセント、ディセントを含めた すべて をスケールさせるという点だ。つまり、ウェブフォントに横幅を合わせようとしてシステムフォントを 110% に拡大すると、意図せずシステムフォントの縦の高さまで 110% 大きくなってしまい、結果として縦方向のレイアウトシフトが発生してしまう!

これを直すために、各オーバーライド (ascent-override, descent-override, line-gap-override) は、元のフォントサイズではなく size-adjust の値に対するパーセンテージとして計算される。size-adjust を使って横幅を完璧に合わせたあと、オーバーライドメトリクスを使って縦の高さを押しつぶす(あるいは引き上げる)ことで、カスタムフォントの正確なバウンディングボックスに合わせるというわけだ。

実際のコードはこんな感じになる。ローカルの Arial フォントを数学的に正確なオーバーライドでラップし、Roboto-Fallback という全く新しい架空のフォントファミリーを作り出している点に注目してほしい。

/* 1. Load your actual web font */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto.woff2') format('woff2');
  font-display: swap;
}

/* 2. Create the Frankenstein fallback */
@font-face {
  font-family: 'Roboto-Fallback';
  src: local('Arial');
  /* Stretch Arial horizontally to match Roboto */
  size-adjust: 104.5%; 
  /* Squish Arial's vertical metrics to match Roboto */
  ascent-override: 89.92%;
  descent-override: 23.44%;
  line-gap-override: 0%;
}

/* 3. Use them together in your CSS */
body {
  font-family: 'Roboto', 'Roboto-Fallback', sans-serif;
}

ページがロードされると、ブラウザは即座に Roboto-Fallback を描画する。中身はただのローカルの Arial だが、Roboto と数学的に全く同じフットプリントになるように横に引き伸ばされ、縦に押しつぶされている。本物の Roboto ファイルのダウンロードが終わるとフォントが切り替わり、文字のデザインは変わるが、レイアウトは1ピクセルたりとも動かない。

現場でのリアルな話

「キャンセル」ボタンの罠

ある大手ECプラットフォームは、カートの放棄率の高さと、激怒する顧客からのサポートチケットに頭を抱えていた。ユーザーから「間違ってカートを空にしてしまった」というクレームが相次いでいたのだ。エンジニアリングチームがユーザーのセッションを録画して調査したところ、犯人が見つかった。そのサイトは重いブランドフォントをロードしていた。ユーザーは「カートを空にする」ボタンのすぐ横にある「チェックアウト」ボタンを見つける。そして「チェックアウト」をクリックしようとマウスのボタンを押し下げたまさにその瞬間、ブランドフォントがロードされたのだ。そのブランドフォントはデフォルトのシステムフォントよりかなり横幅が広かった。テキストがリフローし、「チェックアウト」ボタンが右に押し出され、「カートを空にする」ボタンが、振り下ろされたマウスクリックの真下に移動してしまったのだ。size-adjust とメトリクスのオーバーライドを実装してフォント切り替え時のボタンの位置をロックしたところ、誤操作によるキャンセル率は即座に低下した。

ニュース記事の大地震

ある人気テックブログが大規模なリニューアルを行い、長文を読むための美しくてモダンなセリフフォントを導入した。表示速度を優先するため、彼らは font-display: swap を使用した。しかし、回線が遅い読者たちは不快な現象を経験することになった。記事をロードし、3段落スクロールして読み始める。2秒後、セリフフォントがロードされる。このセリフフォントはデフォルトのシステムフォントよりも ascent と descent が大きかったため、テキストの全行がほんの数ミリずつ大きくなったのだ。これが3段落にわたって積み重なった結果、巨大な縦方向のズレとなり、ユーザーが読んでいた文章が画面の外へ完全に弾き飛ばされてしまった。ローカルのフォールバックフォントに ascent-override と descent-override を追加したことで、テキストはその場で完璧に切り替わり、読者の視線をあるべき場所にとどめることができた。

Lighthouseスコアの悲劇

あるフロントエンド開発者が、Google の Core Web Vitals に向けてウェブアプリを最適化するために3週間を費やした。画像を WebP に圧縮し、重要でない JavaScript を遅延させ、CSS をミニファイした。自信満々で Google Lighthouse を走らせ、完璧な 100 点を期待した。しかし、結果は 75 点だった。レポートでは、巨大な Cumulative Layout Shift のペナルティが指摘されていた。このレイアウトシフトはすべて、カスタムウェブフォントがロードされたときに、たった1つの <H1> の見出しが1行から2行に折り返されたことが原因だったのだ。ヘッダーのデザインをやり直したり、テキストを不可視にしたりする代わりに、開発者は見出しフォントのための正確なフォールバックメトリクスを生成した。レイアウトシフトはゼロになり、Lighthouse のスコアは 100 点に到達し、開発者はついに安眠することができた。

よくある間違いと落とし穴

  • パーセンテージを勘で設定する: この数字を目分量で決めることは不可能だ。計算には、ウェブフォントとシステムフォントの両方のファイル内のバイナリテーブルから、正確な UPM (Units Per Em)、アセント、ディセント、ラインギャップのデータを直接抽出し、比率計算をして掛け合わせる必要がある。数学的に正しい値を得るためには、常にプログラムのジェネレーターを使おう。
  • ウェブフォントにオーバーライドを適用してしまう: 典型的な罠は、カスタムウェブフォント の @font-face 宣言に直接 size-adjust を追加してしまうこと。これでは何の意味もない。オーバーライドは、local() のシステムフォントを読み込む完全に独立した @font-face 宣言に対して適用し、それを font-family スタックの2番目に配置しなければならない。
  • 骨格が真逆のフォールバックを選んでしまう: フォントメトリクスでできることには限界がある。もしカスタムフォントが超極細のサンセリフ体なのに、ローカルのフォールバックのベースに横幅の広いセリフフォントを選んでしまったら、size-adjust の計算によってセリフフォントが完全に歪んでしまい、切り替わりを待つ間に読めなくなってしまう。ウェブフォントと大まかに似た骨格を持つローカルのシステムフォントを常に選ぶようにしよう。
  • line-gap-override を忘れる: 開発者は幅と高さにばかり気を取られ、ラインギャップは normal のままでいい、あるいは省略してしまっても問題ないと思い込みがちだ。もしブラウザがフォールバックフォントにデフォルトの行間(レディング)を追加し、それがウェブフォントの内部的な行間と完全に一致しなかった場合、縦のリズムが崩れてしまう。常に明示的に定義するようにしよう。

なぜ気にしておくべきなのか

ユーザー体験とSEOが重要になるプロジェクトでカスタムウェブフォントを使おうと決めた瞬間に、フォントフォールバックメトリクスについて考えるべきだ。

歴史的に見て、ウェブ上のタイポグラフィは、美しさ(良いフォントを使うこと)とパフォーマンス(レイアウトのジャンプを避けること)の間の妥協の産物だった。CSSフォントメトリクスの登場により、その妥協は過去のものとなった。サイトの Core Web Vitals を監査しているとき、高い CLS スコアに悩まされているとき、あるいは画面上で少しもガタついては困る再利用可能なUIコンポーネントを作っているとき、フォントフォールバックのオーバーライドは決定的なエンジニアリングの解決策になる。これは、ウェブフォントを「ロード時の厄介者」から、シームレスで気づかれないほどの滑らかなトランジションへと変えてくれるのだ。

さらに深掘り

  • MDN Web Docs: size-adjust - size-adjust 記述子がどのようにグリフのアウトラインやメトリクスをスケールさせるかについての、Mozillaの公式ドキュメント。
  • MDN Web Docs: ascent-override - フォントのアセントメトリクスをオーバーライドする方法についての深掘り。
  • web.dev: Improved font fallbacks - これらズバリの CSS 記述子を使って Cumulative Layout Shift を修正する方法を解説した、Googleの素晴らしいガイド。
  • W3C CSS Fonts Module Level 4 - フォントメトリクスのオーバーライドに関する正確な数学的計算とブラウザのレンダリングルールが詳しく記載された公式仕様。

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: フォントフォールバックジェネレーター (CLS)