公開 文責 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ページ目以降はキャッシュが効く |
| ユーザー投稿など、事前に文字が決まらない | 向かない。収録漏れが常に起きる |
| 帯域のコストを自分で持ちたくない | 向かない。転送は自サイトの帯域になる |
このサイトは記事も見出しも自分で書いていて、出てくる文字が事前に確定します。だから焼き固める方式が使えました。逆に、コメント欄や投稿機能がある構成では、未収録の文字が常に出るので同じやり方はできません。
関連記事
Netlifyのデプロイ失敗と本番チェックの誤検知を切り分けた記録
PRのプレビューと本番デプロイがNetlify側の内部エラーで失敗し、GitHub Actionsのliveチェックまで誤って落ちました。ログの見方、APIでは取れない情報、再試行を入れた修正の記録です。
- Netlify
- 運用
- Astro
Netlify BlobsでPVを数える設計とコスト対策
静的サイトの人気記事順を自前のPVで並べるために、Edge FunctionとNetlify Blobsで閲覧数を数えた記録。書き込み上限、毎時の集約、二重起動の防ぎ方、止まったときの気づき方まで。
- Netlify
- Astro
- 運用
Astroブログでads.txtを設置して確認する手順
Astroのpublicディレクトリにads.txtを置いて本番で応答を確認した記録です。設置できても審査は別問題という実例と、404やHTMLが返る場合の切り分けをまとめます。
- AdSense
- Astro
- Netlify