自社サイトの表示速度を Lighthouse 52 → 89 に改善した話 — Claude と一緒に「効かない施策」を捨てながら
こんにちは、クリエイティブSecの長谷川です。
最近、自社サイト(まさに、このサイトです)の表示速度が遅いという声が社内から上がってきました。あまり気にしたことはなかったけど、実際どうなのだろうと計測してみたところモバイルの Lighthouse Performance スコアが 52 でした。
そこから少しずつ改善に取り組んで、最終的には 89 まで持っていきました。総転送量は 約 1,009KB → 640KiB、CLS(レイアウトのガタつき)は 0 を維持したままです。
今回は、その過程で何が遅かったのか、どう直したのか、そして AI(Claude)をどう使ったのかを、実測値つきで書いていこうかなと思います。うまくいった話だけではなく、「効くと思ったのに効かなかった施策」や「AI が間違えた場面」も正直に書いていきます。そちらのほうが、実務ではきっと役に立つ気がするので。
先に、計測条件をそろえておきます
Web の表示速度は、測り方ひとつで数字が変わってしまうので最初に条件を固定しておきました。
- Lighthouse 13.3.0 / モバイル / シミュレーションスロットリング(Slow 4G 相当)
- 3 回計測して、その中央値を採用する
- 計測マシンの負荷で数字がブレるので、benchmarkIndex 2400 未満の回は無効として捨てる
- 毎回ページキャッシュをパージして、2〜3 回空読みで温めてから測る
正直に白状しておくと・・・序盤は「3 回のうち最良値」で記録していた時期がありました。なので 52 → 89 という数字には、測定基準の違いが少し混ざっています。同じ基準(中央値)で確実に言えるのは、後半の 83 → 86 → 89 の部分ですね。
Before:もともとどうなっていたか
WordPress で構築した、ごく普通のコーポレートサイトです。納品時から大きな改修はせず、数年そのまま運用してきました。
| 項目 | 当初 |
|---|---|
| Performance(モバイル) | 52 |
| 総転送量 | 約 1,009KB |
| 外部タグ | GTM / GA4 / Microsoft Clarity |
「画像が重いんだろうな」くらいの見当はついていたのですがどこがどれだけ効いているのかは、まったく分かっていませんでした。まずはここが出発点になります。
まず、素直に効くものから潰していく(52 → 83)
一番の重しは、使っていない計測タグでした
いきなり最大の要因が見つかりました。Microsoft Clarity です。
GTM 上で停止しただけで、スコアが +25 上がりました。使っていないのに、読み込まれ続けていたわけですね・・・。
続けて、GTM / GA4 そのものを遅延注入に変えました。load イベントの 2 秒後、またはユーザー操作(scroll / touchstart / keydown / click)をきっかけにgtm.js を注入する方式です。これで 82 → 88 → 91 まで伸びました。
ただ、実装にはいくつか罠がありました。このあたりは同じことをやる方の参考になるかもしれないので、書いておきます。
dataLayerの初期化とgtm.startの記録は即時にやる(遅らせると計測タイミングがずれる)pageLoadedガードが必須。bundle.js(Swiper / GSAP)の読み込み中に scroll が発火するため、ガードが無いと load 前に注入されてしまうrequestIdleCallbackのtimeoutは「上限」であって「待機時間」ではない。確実に遅らせたいならsetTimeoutで包むmousemoveはトリガーに入れない(デスクトップで勝手に発火してしまう)
計測タグは、入れた本人が思っているよりも高くつきます。一度、棚卸しをしてみるのをおすすめします。
「WebP にすれば軽くなる」は、いつも正しいわけではない
ここが、今回いちばんの発見でした。
サイト全体の .webp を、元ファイルとサイズ比較してみたところ50 本が、元より大きくなっていました。無駄は、合計で 292KiB。いちばんひどいものが、これです。
about_obj_dot.png 632 B → .webp 20,406 B (32.3 倍)
concept_obj_dot.png 880 B → .webp 49,388 B (56.1 倍)
原因は、lossy(非可逆)で変換していたことでした。ドット柄やイラストのような、平坦な塗り分けの PNG はlossy WebP と相性が最悪なんですね。逆に lossless(可逆)で変換すると、PNG より小さくなり、しかも画素が完全に一致します。
| ファイル | PNG | lossless WebP | lossy WebP |
|---|---|---|---|
| concept_obj_dot@2x.png | 4,679 | 3,326 | 33,254 |
| concept_obj_dot.png | 880 | 690 | 13,066 |
| page_top_btn@2x.png | 1,827 | 1,632 | 3,260 |
結局、57 本を lossless で作り直して、473,260 B → 151,416 B になりました。全ファイルで compare -metric AE を実行して、元画像と 1 画素も違わないことは確認しています。
このサイトは nginx が Vary: Accept で WebP を優先配信していたのでブラウザは、膨らんだ側をずっと受け取り続けていました。なかなか気づきにくい種類の劣化だと思います。
そのほか、細かいところ
大きな 2 つ以外にも、地味に効くところを潰していきました。
- 不要な CSS / JS の dequeue と、JS の defer 化
- 画像の二重取得の解消(
<img src>とスクリプトの XHR で、2 回落ちていました) - IE 対応の終了と picturefill の除去(
bundle.jsが −32.5KB) - CSS の未使用セレクタ整理と minify(非圧縮 190KB → 168KB)
ここまでで、スコアはいったん 83 まで来ました。
頭打ちになったので、原因を特定しに行く
ところが、ここで スコアが 83〜86 で止まってしまいました。軽くしても、もう伸びない。何かがボトルネックになっている、ということですね。
そこで Chrome DevTools の Performance トレースを取って、分解してみました。出てきたのが、これです。
0ms ナビゲーション開始
702ms theme.css / bundle.js / lazysizes / kv_illust / kv_pic を一斉要求
↓ Slow 4G の帯域 180KB/s を、約 217KB が奪い合う
1,918ms theme.css(26KB)到着 ← レンダリングの門
1,921ms UpdateLayoutTree(111ms / 要素数 1,051)
2,032ms Layout(1,314ms)★
対象オブジェクト 1,357 個のフルレイアウト
3,346ms レイアウト完了 → ペイント
この 1 発の Layout(1,314ms)が、すべての指標の正体でした。
正確には、こういうことです。
- ページ全体の Layout は 390 回・合計 1,598ms。うち 1,314ms が、この 1 発
- このタスクの内訳の 99% が Layout。JavaScript は 1 バイトも実行されていない
- TBT(Total Blocking Time)は「FCP 以降のタスクの 50ms 超過分の合計」なので、この 1 発だけで 474ms を計上してしまう
- LCP が伸びないのも、このレイアウトがペイントを塞いでいるため
さらに「では、軽くすればどこまで速くなるのか」も実測してみました。DevTools でリクエストをブロックして、純粋なレイテンシを測ります。
style.css 1KB → 707ms ← ほぼ純粋なレイテンシ
theme.css 26KB → 1,093ms
差分 25KB → 386msSlow 4G では、1KB 運ぶのに 707ms かかります。つまり theme.css が回線を独占できたとしても約 925ms が下限で、競合を全部排除しても、改善の上限は約 170ms しかないということになります。
これは、なかなか重要な結論でした。「軽くすれば速くなる」には天井があると、数字ではっきり分かったからです。
構造が分かったので、削るところを選ぶ
上限が分かったので、あとは「安く効くもの」から選んでいきます。
ドロワーの背景画像(−85KB)
ハンバーガーメニューの背景画像が、メニューを開かなくても、全ページで読み込まれていました。.c-drawer は常に DOM にあるので、background-image を無条件に書くと、こうなってしまうんですね。
/* 開いたときに、初めて取得させる */
.c-drawer.is-act .c-drawer__head {
background-image: url(../../img/common/parts/drawer_pic.jpg);
}favicon(17,542 B → 944 B)
中身が、16 / 24 / 32 / 48 の 4 サイズすべて、無圧縮 32bit BMP でした。16 と 32 だけを PNG 圧縮で再構成して、18 分の 1 に。元の 16 / 32 のデータをそのまま取り出して使ったので、画素は完全に一致しています。
ウェブフォントのサブセット化(74,624 B → 23,760 B)
英字見出しに使っている書体が、woff2 で 74.6KB ありました。しかも優先度 VeryHigh で、CSS の直後にクリティカル期間へ割り込んできます。LCP 要素のメイン画像(68KB)より大きいリソースだったわけです。
このフォントは Latin 専用で、日本語は font-family のフォールバックが描画しています。そこで 主要 14 ページを走査して、実際に描画される文字を全部数えました。
78 種:
␣#&()-./0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYabcdefghijklmnoprstuvwyz|~©»×―“”…→納品時のフォントは 649 コードポイント / 965 字形。実際に使うのは、78 文字だけでした。© » × ― " " … → が含まれるので ASCII だけでは足りませんが、Latin-1 補助まで含めても 215 コードポイント / 249 字形で足ります。
結果、74,624 B → 23,760 B(−68%)。
見た目が変わっていないことは、こうやって確認しました。
| 検証 | 結果 |
|---|---|
| 縦メトリクス(ascent / descent / lineGap / x-height / cap-height) | 完全一致 |
| 送り幅(advance width) | 215 文字すべて一致 → レイアウトがずれない |
| HarfBuzz による実シェーピング(21 文例) | 総幅・字形位置とも一致(カーニング保持) |
| ラスタライズ比較(18〜64px の 6 文例 × 2 ウェイト) | 差分画素 0 |
効かなかった施策(実装する前に捨てたもの)
ここが、今回いちばん価値のあった部分かもしれません。「効きそう」に見えて、実測したら効かなかった仮説が 6 件ありました。
| 仮説 | 検証方法 | 結果 |
|---|---|---|
sizes="auto" が layout thrashing を起こしている | auto ↔ 固定値の A/B(6 run) | 無関係。全 run で同位置・同長のタスク |
picturefill が w 記述子と競合している | 削除して計測 | 無関係。bundle は 32.5KB 減ったが TBT は不変 |
| インライン SVG(704 要素)が初回レイアウトを重くしている | トレースで DOM 構造を確認 | 誤り。初回は <img> 1 個。704 要素になるのは load 後で 4ms |
画像の二重取得は nginx の Vary: Accept が原因 | curl -I で Accept を変えて比較 | 誤り。原因はスクリプト側の実装 |
| HTTP/2 が無効で優先度制御が効いていない | DevTools の Protocol 列 | 誤り。h2 で有効だった |
bundle.js を load 後に注入すれば改善する | 該当リクエスト計 120KB をブロックしてトレース | ほぼ無効。CSS 到着が 1,918 → 1,869ms(−49ms のみ) |
最後の 1 件は、もし実装していたらSwiper / GSAP などをすべて load 後に動かすことになり、Speed Index の悪化と、CLS 0 の崩壊リスクがありました。実装する前にブロックテストで潰せたので、数時間と品質の両方を守れたわけですね。
もう 1 件、「クリティカル CSS のインライン化」も見送りました。かなり定番の施策なのですが、実装前に上限を測ってみたところ、
- インライン化しても、あの 1.3 秒の Layout は 1ms も減らない(全 8 run で 1,224〜1,438ms、差なし)
- gzip 後のバイト数は変わらない(39,011 B → 38,785 B)
- TTFB がむしろ増える(79ms → 197ms。nginx が大きい HTML を都度 gzip するため)
得られるのは、往復 1 本ぶんだけ。一方でコストは、テンプレート別の抽出・ビルド改修・CLS 0 が崩れるリスク。これは割に合わないな、と判断しました。
定番だからやる、ではなく、自分のサイトで上限を測ってから決める。これが、今回いちばんの教訓だった気がします。
After:結果
| 指標 | 当初 | 現在 |
|---|---|---|
| Performance(モバイル) | 52 | 89 |
| 総転送量 | 約 1,009KB | 640KiB |
| First Contentful Paint | — | 1.5s |
| Largest Contentful Paint | — | 2.4s |
| Speed Index | — | 3.2s |
| Total Blocking Time | — | 300ms |
| Cumulative Layout Shift | — | 0 |
| Best Practices | — | 100 |
| Accessibility | — | 89 |

CLS 0 と Best Practices 100 は、改善の全期間を通して一度も崩していません。速度のために表示品質を犠牲にしない、というのは最初に決めておいた方針でした。
正直に書いておくと、Performance スコアには無視できないブレがあります。この環境では、例の 1.3 秒の Layout が FCP の前後どちらに落ちるかでTBT が 300ms と 800ms のあいだを行き来して、スコアが 12 ポイントほど振れます。
| TBT | Performance |
|---|---|
| 255〜352ms | 87〜92 |
| 746〜893ms | 78〜80 |
なので最終的な判断は、決定的な値である「総転送量」と「CLS」で行っています。スコアの上下だけを見て、一喜一憂しないほうがいいですね。
Claude を使ってみて、どうだったか
ここからが、もしかしたら本題かもしれません。今回の作業は、AI(Claude Code)と一緒に進めました。どう役に立って、どこで気をつけるべきだったかを書いていきます。
効いたこと:計測結果を「全部」読める
Lighthouse のレポートは、HTML で見ると要約しか出てきませんが中身は JSON で、全リクエストの転送量・優先度・開始/終了時刻が入っています。
人間が目視で追うのは、正直、現実的ではない量です。ですが Claude なら、全部読んで集計できます。
膨張 WebP 50 本を見つけられたのも、「実際に描画される文字は 78 種」を主要 14 ページの走査で確定できたのも、全数を機械的に当たれたからでした。サンプリングでは、たぶん出てこなかった結論だと思います。
効いたこと:効かない施策を、実装前に潰せる
これが、個人的には最大の効果だと思っています。
先ほど書いたとおり、6 件の仮説を実測で否定して、定番施策 1 件(クリティカル CSS)も見送りました。どれも「やってみて、ダメだったので戻す」をやると、半日〜数日が飛んでいく類のものです。
Claude と一緒だと、「これを検証するには何を測ればいいか」という設計と、その測定を実際に回すところまでが、速く回ります。DevTools でリクエストをブロックして上限を測る、という発想も含めてですね。
効いたこと:検証つきで変更できる
フォントのサブセット化は、普通なら「見た目が変わっていないか目視で確認」で終わりがちです。ですが今回は、そこに加えて、
- 全 215 文字の送り幅の一致(これがずれると、レイアウトが動きます)
- HarfBuzz による実シェーピング比較(カーニングが保たれているか)
- ラスタライズして、画素単位で差分をとる(結果は差分 0)
まで自動でやりました。「たぶん大丈夫」を「差分 0 です」に変えられるのは、大きいと思います。
気をつけること:AI は、推測を断定として書く
ここは、正直に書いておきます。
膨張 WebP を見つけたとき、Claude は「これは EWWW Image Optimizer というプラグインが生成したものだ」と断定して、引き継ぎメモにも手順書にも、そう書きました。根拠は「.webp のタイムスタンプが 8/3、元の PNG は 2021 年」という状況証拠だけだったんですね。
私がその設定を探しても見つからず、「そんな設定はないよ」と伝えたところ、Claude はプラグインのソースを読みに行って、自分の記述が誤りだったと訂正しました。
// EWWW の「最適化するフォルダー」は ABSPATH 配下しか受け付けない
if ( ( $abspath && strpos( $path, ABSPATH ) === 0 ) || ... )問題の /img/ ディレクトリは ABSPATH の一つ上にあって、そもそも、このプラグインからは手が届かない場所でした。つまり、犯人ではなかったわけです。
もし私が指摘しなければ、間違った対策手順が、ドキュメントにそのまま残っていました。
教訓は、2 つあると思っています。
- AI の出力は「推測」と「検証済み」が、同じ口調で出てきます。根拠を聞く、裏を取らせる、という一手間を省いてはいけないですね
- 一方で、指摘すればソースまで読んで、自力で訂正できます。「なんだか怪しいな」と思ったら、遠慮なくぶつけたほうがいいです
似た場面は、もう一度ありました。プラグインの除外設定に img と入れたところ、これが <img に部分一致してサイト全体の遅延読み込みが無効化され、画像が 174KB ぶん余計に読み込まれました。このときも、原因の特定はプラグインのソースを読んで確定させています。「たぶんキャッシュでしょう」で終わらせずに済んだのは、コードを当たれたからですね。
良かった運用:引き継ぎメモを書かせる
作業は、複数日・複数セッションにまたがりました。そこで AI に引き継ぎメモを書かせて、次のセッションの最初に読ませる運用にしました。
このメモには「効いた施策」だけではなく、「効かないと実証済みの仮説」と「過去の記述のうち、誤りと判明したもの」を必ず残します。おかげで、同じ検証を二度やることも、間違った前提で作業を始めることもありませんでした。
まとめ
長くなってしまったので、要点だけ振り返っておきます。
- 外部タグの棚卸しは、たぶん一番費用対効果が高いです。 使っていない計測タグが 1 本あるだけで、スコアが 25 落ちていました
- 「WebP にすれば軽くなる」は、いつも正しいわけではありません。 平坦な画像に lossy WebP は逆効果で、lossless なら画素一致で小さくなります
- 軽量化には天井があります。 回線条件によってはレイテンシが支配的で、バイト削減の効果には上限がある。まずは測ってみるのがよさそうです
- 定番施策でも、自分のサイトで上限を測ってから決める。 今回はクリティカル CSS を「効かない」と判断して見送りました
- AI は「全数を当たる」「実装前に上限を測る」作業と相性が良いです。 ただし推測を断定として書くので、根拠を確認する一手間は必須ですね
そして、まだ本丸が残っています。1,357 オブジェクトのフルレイアウトに 1,314ms かかっている部分で、これは単独の主犯がおらず、DOM 全体 × CSS の総量の問題です。短期で大きく削るのは難しく、腰を据えて取り組む必要がありそうです。それについては、また別の機会に書ければと思います。
さいごに
いかがでしたでしょうか?
今回いちばんお伝えしたかったのは、「速くする」こと自体よりも、効くか効かないかを、思い込みではなく実測で決めるという進め方のほうかもしれません。定番の施策ほど、いったん自分の環境で上限を測ってみる。そうすると、やらなくていい仕事がけっこう見えてきます。
そして、その「手数を増やす」ところで AI がよく効きました。丸投げするのではなく、検証の手数を増やす道具として使うというのは、開発以外の業務にも応用が利く気がしています。
Auto-ID フロンティアでは、業務システム・Web システム・スマホアプリの開発に加えて、AI を実務にどう組み込むかのご相談もお受けしています。これまでのAI活用の導入事例もまとめていますので、今回のような使い方にご興味があれば、お問い合わせフォームからお気軽にご相談いただけると嬉しいです。
それでは、今回はこのへんで。




