Astro + microCMSのブログをLighthouse56→100点にした話
個人ブログ(息子の闘病記録)をAstro + microCMS + Cloudflare Pagesで構築したあと、 最後にLighthouseを測ったらモバイルのPerformanceが 56 でした。 ここから2段階の最適化で100まで持っていけたので、やったことと数字をまとめます。
公開URL: https://west-blog.pages.dev/
TL;DR
| 段階 | Performance | FCP | LCP | 総転送量 |
|---|---|---|---|---|
| 改善前 | 56 | 9.9 s | 9.9 s | 1,105 KiB |
① Google Fontsを&text=でsubset 化 |
99 | 1.6 s | 1.6 s | 189 KiB |
| ② フォントを自前ホスト+CSSインライン化 | 100 | 0.8 s | 0.8 s | 188 KiB |
- すべてLighthouse Mobileの数値
- 3回連続で100が安定して出ているのを確認
- デザインは一切変えていない(Noto Serif JP/Noto Sans JPのまま)
改善前の状態
最初の計測で出てきたのはこんな感じでした。

Performance : 56
Accessibility : 100
Best Practices : 100
SEO : 100
FCP / LCP / Speed Index / TTI : 全部 9.9 s
TBT : 0 ms
CLS : 0Accessibility / Best Practices / SEO は全部満点でしたがPerformanceで詰まりました。 全ての時間指標がきっかり 9.9 sで揃っているのが怪しかったので、ネットワークの内訳を見ました。
ネットワーク転送量の上位16リクエストのうち、15 個がfonts.gstatic.com です。
178KB Google Fonts CSS(fonts.googleapis.com/css2?...)
105KB Noto Serif JP subset(woff2)
77KB Noto Sans JP subset
32KB Noto Serif JP subset
25KB Noto Serif JP subset
... あと10個くらいsubsetが続きました主犯はCJK フォント。
Google Fontsは Noto Sans JP / Noto Serif JPのような巨大な日本語フォントをunicode-rangeで多数のサブセット (.woff2) に分割して配信してくれます。便利な仕組みですが、ページに出てくる漢字・かな・記号のsubset群を都度ダウンロードする結果、ファイル数が膨らみ、全部届くまでに時間がかかります。
ステップ① ― Google Fonts text=パラメータでsubset 化
Zennのこちらの記事 で知ったのですが、Google FontsのリクエストURLに&text=あいう...のように使われる文字を全部渡すと、その文字だけを含む小さなwoff2を1ファイルで返してくれます。
SSGならビルド時にdist/ のHTMLを全部読めば、使われている文字が確定できるので試してみようと思いました。
Astro Integrationとして実装
// src/integrations/google-fonts-subset.mjs (抜粋)
export default function googleFontsSubset() {
return {
name: 'google-fonts-subset',
hooks: {
'astro:build:done': async ({ dir, logger }) => {
const distDir = fileURLToPath(dir);
const htmlFiles = findHtmlFiles(distDir);
// 1.全ページの<body>からユニーク文字を集める
const chars = new Set();
for (const file of htmlFiles) {
const html = fs.readFileSync(file, 'utf8');
const text = decodeEntities(extractBodyText(html));
for (const ch of text) {
if (/\s/.test(ch)) continue;
chars.add(ch);
}
}
const subset = [...chars].sort().join('');
const encoded = encodeURIComponent(subset);
// 2.各HTMLのGoogle Fonts URLに&text=<chars>を追記
const fontsUrlRegex =
/(href=")(https:\/\/fonts\.googleapis\.com\/css2[^"]*?)(")/g;
for (const file of htmlFiles) {
let html = fs.readFileSync(file, 'utf8');
html = html.replace(fontsUrlRegex, (m, open, url, close) => {
const sep = url.includes('&') ? '&' : '&';
return `${open}${url}${sep}text=${encoded}${close}`;
});
fs.writeFileSync(file, html);
}
},
},
};
}astro.config.mjs に追加します。
import googleFontsSubset from './src/integrations/google-fonts-subset.mjs';
export default defineConfig({
integrations: [sitemap(), googleFontsSubset()],
});結果(ステップ①時点)
| Before | After ① | |
|---|---|---|
| Performance | 56 | 99 |
| FCP / LCP | 9.9 s | 1.6 s |
| Google Fonts のリクエスト数 | 15+ | 1〜2 |
| 総転送量 | 1,105 KiB | 189 KiB |
Performanceは満点直前まで来ましたが、Lighthouseを3回連続で回すと 85 / 99 / 99 とぶれることが分かりました。 1回目は遅い、2回目以降は速い。初回はキャッシュ(コールド)の問題かな?と思いました。
PageSpeed Insights(PSI)の結果も87 / 携帯電話 だったので、これは「初回訪問の体感」を表しています。 さらにPSIの指摘で2つ追加課題が見えました。
- レンダリングをブロックしているリクエスト — Google Fonts CSSを読みに行く 750msがrender-block
- 効率的なキャッシュ保存期間を使用する —
fonts.gstatic.comはmax-age=86400(1日)。長くしたい。
ステップ② ― フォント自前ホスト + CSS インライン化
text=で軽くした CSSとwoff2をCloudflare Pagesでホストしてしまえば、
- CSSの取得待ち(render-block)が消える(HTMLに直接埋め込めばよい)
- キャッシュ期間を自分で設定できる(Cloudflare 経由で
max-age=31536000, immutable)
の2つを一気に解決できるはず。Integrationを拡張しました。
やったこと
- ビルド時に**
&text=...付きのGoogle Fonts CSSをfetch** - CSS 内の
fonts.gstatic.comのURLを全部fetchしてwoff2ダウンロード - woff2を
dist/_fonts/font-<contentHash>.woff2として保存 - CSS の
@font-faceのsrcURL を/_fonts/font-XXXX.woff2に書き換え - HTMLの
<link rel="stylesheet" href="fonts.googleapis.com/...">と<link rel="preconnect">を全削除 - 書き換えたCSSを
<style>でインライン化して</head>の直前に挿入 public/_headersで/_fonts/*に1年キャッシュを設定
// public/_headers
/_fonts/*
Cache-Control: public, max-age=31536000, immutableハッシュ付きファイル名なので、フォント内容が変わればURLごと変わる →無限キャッシュする。
Integrationのフロー(概念図)
わかりやすいように、フローをAIに書かせてみました。
[astro:build:done]
│
├─ dist/**/*.html を読む
├─ <body> から unique chars 抽出
├─ fonts.googleapis.com/css2?...&text=<chars> を fetch
│ ↓
│ CSS 取得 (各 @font-face は gstatic URL を指す)
├─ gstatic URL の woff2 を並列 fetch
│ ↓
│ dist/_fonts/font-<hash>.woff2 に保存
├─ CSS の URL を /_fonts/<hash>.woff2 に書き換え
├─ 各 HTML の <link href="fonts.googleapis.com|gstatic.com"> を削除
└─ 書き換えた CSS を <style> として </head> 直前に挿入結果(ステップ ② 時点)
3回連続で計測してすべて Performance 100 が出ました。

| 改善前 | After ① | After ② | |
|---|---|---|---|
| Performance | 56 | 99 | 100 |
| FCP | 9.9 s | 1.6 s | 0.8 s |
| LCP | 9.9 s | 1.6 s | 0.8 s |
| TBT | 0 ms | 0 ms | 0 ms |
| CLS | 0 | 0 | 0 |
| 総転送量 | 1,105 KiB | 189 KiB | 188 KiB |
| Google Fonts リクエスト数 | 15+ | 1〜2 | 0(自前ホスト) |
PageSpeed Insightsの警告も両方とも消えました!
計測中に気づいたこと
Lighthouse のスコアは1回では決まらない。 3回計測したら 85 / 99 / 99 という結果になったように±10 点くらいはぶれる。 特に「初回計測」と「2回目以降」で差が大きいので、 複数回計測して中央値で判断する。
PSI が出すスコアは「初回訪問相当」の状態を測っているので、 普段の自分が「2回目以降の速い体感」しか見ていなかったりすると、 PSI 結果といつもの感覚が食い違う。
参考リンク: