Webaff

公開 文責 Webaff 編集部

日本語Webフォントを自己ホストして転送量を削る手順

  • Astro
  • Netlify
  • 表示速度

このブログは本文も見出しも Zen Kaku Gothic New で表示しています。最初は Google Fonts から読んでいましたが、いまは使う文字だけに絞った woff2 を自分で配信しています。きっかけは、トップページのフォント関連の転送が 52リクエスト・553KiB になっていたことです。

数字だけ見ると「Google Fonts が重い」という話になりそうですが、実際はそうではありません。仕組みを理解したうえで、このサイトの条件には合わなかった、というのが正確なところです。

52リクエストになる理由

Google Fonts は日本語フォントを文字の出現頻度で分割して配信します。Google の発表によると、日本語では上位3,000字を20個のスライスに分ける方式を採っており、フォント全体を送る場合に比べてダウンロードは80%減ったとされています。仕組みとしては unicode-range と woff2、それに HTTP/2 の並列取得の組み合わせです。

つまりページに出てくる文字が少なければ、必要なスライスも少なくて済みます。逆に言うと、文字の種類が多いページでは多くのスライスが必要になる。計測したときの内訳がこれでした。

項目 Google Fonts 自己ホスト
リクエスト数 52 3
転送量 553KiB(CSS 85KiB + woff2 468KiB) 396KiB(切替時の計測値)
ページ間の再利用 ページごとに必要なスライスが変わる 全ページで同じ3ファイル

トップページには記事タイトルが並ぶので、漢字の種類が一気に増えます。ブログの一覧ページは日本語サイトの中でもとくに文字種が多くなる場所で、分割配信の利点が出にくい面がありました。

web.dev のフォント最適化の解説も、サブセットを作るときは「ページごとに少しずつ違う文字集合をダウンロードさせない」ことを勧めています。サイト全体で使う文字を1つにまとめれば、2ページ目以降はキャッシュがそのまま効きます。

サイト全体で使う文字を集めて焼く

やっていることは単純で、ソースに出てくる文字を集めて subset-font に渡すだけです。scripts/generate-fonts.mjs が次を作ります。

public/fonts/zen-kaku-gothic-new-{400,500,700}.woff2
public/fonts/OFL.txt      再配布に必要なライセンス本文
public/fonts/subset.json  収録文字の記録

実行するとこうなります。元の TTF は1ウェイト 2.2〜2.3MiB です。

収録文字数: 1230
  zen-kaku-gothic-new-400.woff2  136.7 KiB  (元 2.3 MiB)
  zen-kaku-gothic-new-500.woff2  137.8 KiB  (元 2.2 MiB)
  zen-kaku-gothic-new-700.woff2  138.6 KiB  (元 2.2 MiB)
合計 413.2 KiB -> public/fonts/

ウェイトは3つだけ用意し、font-semibold(600)は700のファイルで描かせています。@font-face の font-weight に範囲を書けるので、ファイルを増やさずに済みます。

@font-face {
  font-family: "Zen Kaku Gothic New";
  font-weight: 600 700;
  font-display: swap;
  src: url("/fonts/zen-kaku-gothic-new-700.woff2") format("woff2");
}

font-display: swap は、フォントの到着を待たずに代替フォントで描き、届いたら差し替える指定です。仕様上は「極小のブロック期間と無限のスワップ期間」で、テキストが見えないまま待つ時間をほぼ作りません。代替に何が使われるかは自分で決めておく必要があるので、"Hiragino Sans", "Noto Sans JP", sans-serif を並べています。

再配布するのでライセンス本文の同梱も必須です。Zen Kaku Gothic New は OFL-1.1 なので、OFL.txt を一緒に置いています。生成スクリプトが毎回ダウンロードして上書きするようにして、消え落ちないようにしました。

収録漏れは静かに起きるので機械で見張る

この方式のいちばんの弱点は、新しく書いた漢字がサブセットに入っていないと、その文字だけ代替フォントで描かれることです。見た目は「一部の字だけ字面が違う」という分かりにくい壊れ方をします。

そこで subset.json に収録文字を記録して、品質チェック(npm run check:content)が未収録の文字を警告するようにしました。警告が出たら npm run fonts:generate を実行して焼き直します。

判定はソースを機械的に走査するので、記事本文だけでなくコード内のコメントに書いた漢字でも警告が出ます。実際、この記事を書いている最中にも出ました。取りこぼすより多めに入れる方向で割り切っています。切替時は1,191字・396KiB で、記事を足した現在は1,230字・413.2KiB。数十字増えても転送量はほとんど変わりません。

自己ホストで増えるコストもある

ここは見落としがちなので書いておきます。Google Fonts から読んでいたときは、フォントの転送は Google 側の帯域でした。自己ホストにすると、その 413KiB は自分のサイトの帯域になります。

Netlify の従量課金は帯域1GBあたり20クレジットなので、キャッシュが効かない初回訪問だけで考えるとこうなります。

1,000,000,000 バイト ÷ 413.2KiB ≒ 初回訪問 約2,400回 = 20クレジット

Free プランの月300クレジットに対して、初回訪問2,400回で20クレジット。当サイトの流入はこれに遠く及びません。2026-09 の検索指標は全記事あわせて表示54回・クリック5回です。表示回数は検索結果に並んだ回数で訪問ではないので、訪問の目安に近いのはクリックのほうです。いまは誤差の範囲ですが、アクセスが増えるほど効いてきます。2ページ目以降はキャッシュから読まれるので、効くのは初回訪問の回数です。無料枠の内訳とデプロイ回数の制約については ほぼ0円で始める広告収益ブログの設計と検証手順 に実費で書きました。

リクエスト数が52から3に減ったこと自体は、初回表示の待ち時間に効きます。ただし「速くなった」と言うには本番での連続測定が必要で、そこはまだ揃っていません。広告を入れたときの測り方は 広告表示がCore Web Vitalsへ与える影響の測り方 に手順としてまとめてあります。

同じことをやるかの判断

日本語フォントを自己ホストする価値があるのは、次のような条件のときです。

条件 自己ホストの向き
1ページに出る文字の種類が多い(記事一覧・長文) 向く。スライスが多数必要になる形を1ファイルにできる
ページ間で同じ文字集合を使い回せる 向く。2ページ目以降はキャッシュが効く
ユーザー投稿など、事前に文字が決まらない 向かない。収録漏れが常に起きる
帯域のコストを自分で持ちたくない 向かない。転送は自サイトの帯域になる

このサイトは記事も見出しも自分で書いていて、出てくる文字が事前に確定します。だから焼き固める方式が使えました。逆に、コメント欄や投稿機能がある構成では、未収録の文字が常に出るので同じやり方はできません。

関連記事