日本語のWebフォントは重い。これはもう、避けようのない事実だと思います。



あるサイトで、Lighthouseのスコアが75点から上がらなくなりました。画像は圧縮した。JSも遅延読み込みにした。それでも上がらない。
犯人はフォントでした。
使う文字だけを切り出して自分のサーバーから配ることにしたら、転送量が536KBから202KBに減り、スコアが93点になりました。手順を書きます。
まず「フォントが重い」を分解する
ここが出発点でした。ひとことで「重い」と言っても、中身は3つに分かれます。
- CSSが重い(86KB)
- フォント本体が重い(450KB)
- 外部ドメインへの接続が発生する(2つ)
Google Fontsから日本語フォントを読み込むと、CSSだけで80KBを超えます。日本語は文字数が多いので、unicode-range で100個以上に分割されているためです。この分割は本来ありがたい仕組みなのですが、その定義自体が重い。
そして fonts.googleapis.com と fonts.gstatic.com の2つに接続が発生します。
よく紹介される対策として、CSSのリンクに media="print" を付けて非同期化する方法があります。これは試しました。Lighthouseのシミュレーション値では、FCPが約2秒遅いままでした。
実際にブラウザで見ると速いのに、スコアが低い。この状態のときは、フォントを疑うといいと思います。
使う文字だけ切り出す
やることは単純です。そのサイトで実際に使う文字だけを含んだフォントファイルを作り、自分のサーバーに置く。
必要なのは2つのライブラリです。
pip install fonttools brotli
元のフォントは、Google Fontsのリポジトリから取れます。OFLライセンスのフォントなら再配布できます。
https://github.com/google/fonts/raw/main/ofl/<family>/<Font>.ttf
手順1:可変フォントの軸を絞る
最近のフォントは「可変フォント」といって、1つのファイルに細いものから太いものまで全部入っています。便利ですが、その分重い。
使うウェイトだけに絞ります。
python -m fontTools.varLib.instancer NotoSansJP.ttf wght=900 -o NotoSansJP-900.ttf
欧文フォントで、太さを2種類使いたい場合は範囲で指定できます。
python -m fontTools.varLib.instancer Archivo.ttf wght=600:700 -o Archivo-600-700.ttf
こうすると1ファイル11KBまで落ちました。CSSで font-weight: 600 700 と書けば、両方のウェイトが使えます。
手順2:使う文字だけ残す
次に、実際に使う文字だけを切り出します。
pyftsubset NotoSansJP-900.ttf \
--text-file=chars.txt \
--flavor=woff2 \
--layout-features='*' \
--output-file=noto-900-subset.woff2
chars.txt に、残したい文字を並べておきます。
手順3:どの文字を入れるか
ここがいちばん悩むところだと思います。うちはこうしています。
かな全域 + ASCII + 全角記号 + 実際に使っている漢字。
これで849字、87KBになりました。
「将来使いそうな漢字」を足したくなりますが、うちは足していません。試しに足してみたら1,120字・113KBまで膨らんで、26KB増えた割に得るものがなかったからです。
というのも、サブセットに入っていない文字は、フォールバックのフォントで表示されます。游ゴシックなり、端末に入っているフォントで出る。文字化けはしません。
だから「あとで文言を変えたら文字が消えた」という事故は起きません。少し見た目が変わるだけです。
ただしかなと記号は全部入れておいてください。文言を変えると増えるのは漢字だけなので、かなが揃っていれば、たいていの修正に耐えます。
やってみて効かなかったこと
ここからが本題かもしれません。良さそうに見えて、実際には遅くなったことが2つあります。
preloadすると遅くなった
<link rel="preload"> でフォントを先読みする方法があります。速くなりそうですよね。
やってみたら、FCPが1.7秒から2.1秒に悪化しました。LCPも0.3秒遅くなりました。
理由は、たぶんこうです。和文フォントは87KBあります。これを最優先で取りにいくと、その分だけ他のリソースが後回しになる。結果、最初の描画が遅れる。
font-display: swap に任せて、游ゴシックで先に描いてから差し替える方が速いという結論になりました。
一方、欧文の小さいサブセット(46KB)はpreloadして問題ありませんでした。サイズが小さいので、他を押しのけません。同じpreloadでも、ファイルサイズで結果が変わります。
ファーストビューだけ切り出す小技も効かなかった
「最初に見える範囲の文字だけ数KBに切り出して、それだけ先に読ませればいいのでは」と考えました。
これも試しました。FCPが1.7秒から2.0秒に悪化。
残りのファイルも、結局ページ全体のレイアウトを組むために要求されます。だから読み込みが2段階になるだけで、総量は変わらない。むしろリクエストが増えて遅くなりました。
思いつきそうな手ですが、うちの環境では効きませんでした。
最後の壁はウェイトの数
ここまでやって、最終的に残った制約がこれです。
和文は1ウェイトで62KB。ウェイトの数が、そのまま初期転送量になります。
500・700・900の3つを使うと185KB。ここから先は減らせません。
なので、うちはこうしています。見出しに使う極太(900)だけを自前配信して、本文は游ゴシックに任せる。
そもそも、なぜここまでやるのか
速度の話をしてきましたが、動機は見た目の方でした。
游ゴシックのBoldで組んだ大見出しは、正直「Wordの見出し」に見えます。
日本語サイトの印象を最も左右するのは、見出しの太さだと思っています。ここに極太のフォントを1つ入れるだけで、同じレイアウトでも別のサイトに見えます。
ただ、そのために450KBを読み込むのは重すぎる。
87KBで見た目が変わるなら、やる価値がある。 そういう判断でした。速度と見た目のどちらを取るかではなくて、両方取れる方法を探したという話です。
まとめ
- 「フォントが重い」はCSS・本体・接続の3つに分けて見る
fontTools.varLib.instancerで軸を絞り、pyftsubsetで文字を絞る- 収録はかな全域+ASCII+全角記号+実使用漢字で足りる。未収録はフォールバックで出るので事故らない
- 和文フォントのpreloadは逆効果(FCP 1.7→2.1秒)。
font-display: swapに任せる - ファーストビューだけ切り出す小技も効かなかった(1.7→2.0秒)
- 最後の壁はウェイトの数。1ウェイト62KB
通説どおりにやって遅くなることが、わりとあります。測ってから決める、に尽きるなと思いました。