はじめに
個人開発で電気代比較サイト (エネジェント) を運営しています。Vercel Hobby + Next.js 15 SSG + 月額0円。
PageSpeed Insightsとは
PageSpeed InsightsはGoogleが提供するWebページのパフォーマンス計測ツールです。URLを入力するだけで、4つのカテゴリでスコア (0-100) を返してくれます。
- Performance: ページの読み込み速度・操作可能になるまでの時間 (LCP / TBT / CLSなどCore Web Vitalsを含む)
- Accessibility: スクリーンリーダー対応・色コントラスト・代替テキストなど障害者アクセスの担保
- Best Practices: HTTPS / 安全なAPI利用 / 非推奨API不使用などの一般的ベストプラクティス
- SEO: meta description / canonical / 構造化データ / モバイル対応などの検索エンジン最適化
裏側で動いているのは Lighthouse というオープンソースの監査ツールで、Chrome DevToolsにも同じものが組み込まれています。
なぜスコアを気にするか
PageSpeed Insightsのスコア自体が直接Google検索順位を決めるわけではありませんが、Performanceカテゴリで計測するLCP / INP / CLSの3指標 (Core Web Vitals) はGoogleの正式なランキング要素 です。具体的には次のような影響があります。
- 検索順位: Core Web Vitalsが "good" 範囲に入っていないと、 同じコンテンツ品質でも検索順位で他社に負けます
- 直帰率・コンバージョン: LCPが1秒遅れるだけで離脱率が大きく上がる事例が複数の大規模A/Bテストで報告されています
- AdSense審査・収益: 表示が遅いサイトは審査落ちのリスクや、 表示前離脱で広告インプレッション機会を失います
- Accessibility / SEO: a11yは法令遵守 (海外はADA / EAA、 国内は努力義務) と検索順位の両面で、SEOカテゴリは検索流入の質に直結します
特に個人開発サイトでは大手のドメイン権威で押し勝てない以上、 「読み込みが速くて使いやすい」 という土台で差をつけるしかありません。
この記事の内容
PageSpeed InsightsのPerformanceスコアを69から計測してみた結果、 改善の試行錯誤を経て最終的に 98 / 100 / 100 / 100 に落ち着きました。100を取りに行く一歩手前で止めています。hackを使えば100は取れますが、GA4計測精度を犠牲にしないと無理だと判断したためです。
この記事は、100を取らないと決めるまでの試行錯誤の記録と、その過程で発見したPageSpeed Insightsの計測の落とし穴の話です。
第1幕: LPに記事カードが67枚あった
PageSpeed Insights Web UIを開くと、次のような指摘が並んでいました。
- 使用していないJavaScript: 356 KiB
- TBT 680ms (good範囲は200ms未満)
- メインスレッドで長時間タスク10件
DevToolsで内訳を見たところ、LP (/) を開くと HTMLに <ArticleCard> が67個SSRされている状態でした。
エネジェントは元々 記事一覧ページがなく、LPに記事カードを全件並べることで一覧の代わりにしていました。記事の増加を受け、ハブとなる記事一覧ページを作りましたが、LPの記事カードはそのままにしていました。
つまりLPが 構造的に重複した役割を持ったまま残っていたため、PageSpeed Insightsの数字がそれを正直に映していたともいえます。
LPの役目は「ファネル入口 + シミュレーター誘導」なので、記事を全件並べる必要はありません。まずはシンプルに人気の8記事に絞ることにしました。
※記事の選別方法もひと工夫がありましたので別の話としてまとめます。
結果としてLPの <ArticleCard> を67枚 → 8枚に削減し、PageSpeed Insightsを再計測をしたところ Performance 84。LCPも3.8s → 1.5sまで改善しました。
ここまでで「Performance 90+ は手が届きそう」という感触になり、100を狙ってみることにしました。
第2幕: PageSpeed Insightsスコアが再現しない問題
Performance 100を狙う前に、PageSpeed Insightsスコアの計測を8連続で叩いてみました。
[run 1] fetch=03:31:52Z perf=94 lcp=1508 tbt=235 ← warm
[run 2] fetch=03:31:52Z perf=94 lcp=1508 tbt=235 ← 同じ fetchTime のキャッシュ
[run 3] fetch=03:32:54Z perf=94 lcp=1701 tbt=268 ← warm
[run 4] fetch=03:34:13Z perf=98 lcp=1736 tbt=139 ← warm best
[run 5] fetch=04:02:52Z perf=80 lcp=2456 tbt=772 ← cold (Vercel Edge cache miss)
連続で叩いても2回は同じfetchTimeのキャッシュが返ってきます。違う計測値を取るには20秒以上空ける必要があります。そして空けると、スコアが二極化していました。
- warm: Vercel Edge Networkのキャッシュが温まっている。SSG出力が即配信される
-
cold: PoPのキャッシュミスでorigin (us-east-1) にフェッチしに行く。TTFBが
1-2秒伸びてFCP / LCPも連動して悪化
実ユーザーの大半はwarm体験で、coldは初回訪問・地理的に遠いエリア・キャッシュTTL切れ時のみ通るパスです。つまりPageSpeed Insights Web UIでPerformance 84と表示されたとしても、それがwarmのものなのかcoldのものなのかで意味が大きく変わります。
PageSpeed InsightsでPerformanceを比較するなら 最低5連測 + warm/cold別に集計すべきとわかったので、Pythonスクリプトで集計するようにしました。
runs = []
for i in range(8):
response = call_pagespeed_api(url)
runs.append(response)
time.sleep(20)
groups = {}
for r in runs:
groups.setdefault(r['fetchTime'], []).append(r)
# warm best (perf 最大) を採用
warm_best = max(runs, key=lambda r: r['perf'])
?_cb=<timestamp> でキャッシュバスターを付けるとVercel CDNのcache missを強制できますが、これは実ユーザー体験ではないので別の話になります。
第3幕: 残るボトルネックは3rd partyスクリプト
unused-javascript の中身を引いたところ、ほぼ全部3rd partyでした。
| script | unused | total | 出所 |
|---|---|---|---|
show_ads_impl_fy2021.js |
134 KiB | 176 KiB | AdSense (内部実装) |
gtag.js |
64 KiB | 152 KiB | GA4 |
gtm.js |
57 KiB | 119 KiB | GTM |
adsbygoogle.js |
28 KiB | 53 KiB | AdSense (wrapper) |
1255-...js |
20 KiB | 44 KiB | エネジェント自前chunk |
4bd1...js |
20 KiB | 54 KiB | エネジェント自前chunk |
自前chunkは40 KiBしかなく、削れるとしても効果は限定的です。残りの 95% は AdSense (審査用)・GA4・GTM・Microsoft Clarity といった、サービス運営に必要な計測・収益タグで構成されています。これらは Lighthouse から見ると "unused" にカウントされますが、ユーザー行動分析・広告審査・ヒートマップ取得といった役割を果たしているため、削れば計測機能や収益機会が失われます。
Performance を 100 に近づけるなら、これら 3rd party の「load タイミング」をどうずらすかを試すしかありません。AdSense についていくつか試した結果を以下にまとめます。
試行1: AdSenseを lazyOnload 化
strategy="afterInteractive" を lazyOnload に変えてLighthouse計測窓内にloadされないように試しました。スコアは多少伸びましたが、Lighthouseの計測窓は~12-15秒幅があり、lazyOnloadでも検出されてしまいます。
試行2: requestIdleCallback({timeout:15000}) で完全遅延
<head> にinline loaderを書いて、window.load + requestIdleCallback で更に遅らせました。
var loadAds = function(){
var s = document.createElement('script');
s.src = 'https://...adsbygoogle.js?client=ca-pub-...';
document.head.appendChild(s);
};
var trigger = function(){
if (window.requestIdleCallback) {
window.requestIdleCallback(loadAds, {timeout:15000});
} else {
setTimeout(loadAds, 4000);
}
};
if (document.readyState === 'complete') trigger();
else window.addEventListener('load', trigger, {once:true});
これでLighthouse計測窓外に追い出せそうでした。
立ち止まったポイント: ユーザー体験は何も変わっていない
deploy直前にこの施策の意味を整理し直しました。
- 修正前: AdSenseはhydration直後にload → 広告が早く出る
- 修正後: AdSenseはidle待ち + 15秒タイマー → 広告が遅く出る
この変更でユーザー体験は同じか、むしろ悪化します。AdSenseはいつかはloadされるので、ブラウザが処理するbyte数もJS実行時間も同じです。タイミングをズラしただけで、Lighthouseの計測時刻とload時刻の前後関係が逆転しただけになります。
これは「Lighthouse hacking」と呼ばれる典型パターンで、Lighthouse Issue #11527でSentry開発者と議論されているのと同じ問題です。スコアだけ綺麗にしてユーザーの実体験は変わらない、という指摘がそのまま当てはまる施策でした。
第4幕: 「100を取らない」と決めるための実験
ここまで来て「Performance 100は取れるが取るべきではない」と判断したものの、AdSenseあり / なしで実際に何ポイント変わるのかを定量的に確認していませんでした。これがわからないと「100は実用上不可能」とも「実はAdSenseは影響が小さい」とも言えません。
そこで実験しました。8分間だけAdSenseを一時撤去してPageSpeed Insightsを8連測。その後AdSenseを公式推奨配置 (<head> に静的script tag + strategy="afterInteractive")で復活させ、もう一度8連測しています。
結果
| 指標 | AdSenseなし (a59c5285) |
AdSenseあり (d7d571a0) |
差分 |
|---|---|---|---|
| Performance warm best | 98 | 98 | 0 |
| Performance warm中央値 | 94-98 | 80-98 | -14~0 |
| Accessibility | 100 | 100 | 0 |
| SEO | 100 | 100 | 0 |
| Best Practices | 100 | 100 | 0 |
| 指標 | AdSenseなし | AdSenseあり | 差分 |
|---|---|---|---|
| LCP best | 1.74s | 1.04s | -0.70s |
| TBT best | 139ms | 141ms | +2ms |
| FCP best | 1.06s | 1.04s | -0.02s |
| CLS | 0.014 | 0.014 | 0 |
事前の仮説「AdSense入れるとPerformanceは明確に落ちるはず」は、実測と一致しませんでした。
- warm bestはどちらもperf=98で同じ
- AdSenseありのcoldケースだけperf=80まで落ちる
- LCP bestはむしろAdSenseありのほうが速い (8連測すべてで一貫していました。
AdSenseのpreconnectが他のリソースのDNS/TLS確立にも好影響を与えている可能性)
つまり、
- 「AdSenseあり = Performance必ず落ちる」は不正確
- 「AdSenseあり = Performanceの振れ幅が広がる」が正確
そして決定的だったのは、warm bestはどうしても98で頭打ちだったことです。100に届かない最後の差分は、GTM (gtm.js 122 KB) と GA4 のGoogleタグ (gtag.js 152 KB) の合計script実行で TBT が 200ms 弱押し上がっている分でした。
これは「二重ロード」ではなく、GTM と GA4 を併用する設計上どちらも必要なファイルで、両方を読まないとそれぞれの計測機能が成立しません (Google Tag Manager vs. gtag.js — Tag Manager Help)。 つまり計測機能を持っている以上、避けられないコストです。
仮にこの 200ms を消したい場合、選べるのは次のような選択肢になります。
- (a) GTM をユーザー interaction まで完全遅延 → LP bounce 訪問者の GA4 計測がロスする
- (b) GA4 / GTM 自体を撤去 → そもそもの計測ができなくなる
- (c) GTM を撤去して GA4 を gtag.js のみで直接実装 → GTM 内の 20 種以上のカスタムイベント設定を全部コードに移植する必要があり、その後の運用 (新規イベント追加・タグ管理) が GTM の Web UI から離れる
どれも「Performance スコア +2pt」 のために払うコストとしては割に合わないと判断し、計測機能を維持したまま 98 で止めることにしました。
第5幕: 結論「98 / 100 / 100 / 100で十分」
最終スコアはこのとおりです。
| カテゴリ | スコア |
|---|---|
| Performance | 98 (warm best) |
| Accessibility | 100 |
| Best Practices | 100 |
| SEO | 100 |
| 指標 | warm best |
|---|---|
| LCP | 1.04s |
| TBT | 141ms |
| FCP | 1.04s |
| CLS | 0.014 |
100を取りに行くのを最後の段階で手放しました。取りに行こうと思えば取れますが、そのためにGA4計測精度を犠牲にする価値はないと判断しています。
a11y / SEO / Best Practices = 100はAdSense入っていても達成できました。ここはブランドアセットを変えずにテキスト用バリアントを追加する設計と、sitemap / canonical / hreflang / meta descriptionを整備するだけで取れる範囲です。個人開発者にとってROIが高い領域だと思います。
Performanceはwarm best 90+ を目標にすれば十分で、100を取るためにhackを入れるのは個人開発の時間投資としては効率が悪いと判断しています。LCP / TBT / CLSのCore Web Vitalsがgood範囲に収まっていればGoogle検索順位への影響は出ないので、そこを優先するほうが事業としては筋が通ります。
おまけ: 踏んだ罠
このセッションで実際に踏んだ罠を残しておきます。
1. AdSense動的injectは審査クローラーから見えない
JSで document.head.appendChild() でAdSense scriptをinjectしても、Mediapartners-Google botはraw HTMLを読みに来るため、動的に追加されたscriptを検出できません。
審査中のサイトではAdSense <script> は 静的に <head> に書くしかありません。
2. fetchpriority="low" はasync/defer scriptに対してno-op
Chromeはasync/defer時点でLow priorityになっているため fetchpriority="low" を追加しても何も起きません。
3. PageSpeed Insights APIは内部キャッシュを持つ
同じURLを短時間に叩くと同じ fetchTime のキャッシュが返ります。計測の真値を取るには 1分以上空けて5-8連続計測 + warm/cold別に集計が必須です。
4. Lighthouse audit "Avoid unload event listeners" はPerformance影響なし
AdSense lidar.js が unload listenerを持っていますが、このauditはLighthouse v10でPerformance → Best Practicesに移動しました。
参考
- Adsense and Analytics scripts are hurting Performance score — Lighthouse #11527 (Lighthouse開発者とSentryの議論)
- How to integrate Google AdSense without negative Impact on Page Speed (AdSense入れた状態の現実的上限事例)
- Perfect 100 on PageSpeed Insights — Case Study (2025) (100達成事例)
- Deprecating the unload event — Chrome for Developers