import RelatedArticles from "@/components/RelatedArticles";
「リリースしたら、なぜか初回読み込みが体感で重くなっていた」— Web プロダクトを長く運用していると、誰もが一度は経験する出来事ではないでしょうか。私自身、個人で運営しているサイトで似たことを何度かやらかしました。Lighthouse のスコアを毎日眺める運用は続かず、気づいたときには Largest Contentful Paint がじりじり遅くなっている、という静かな後退が起きるのです。
ここで扱うのはAntigravity の AI と Lighthouse CI を GitHub Actions 上で組み合わせ、「気づいたら遅くなっていた」を構造的に防ぐパイプラインを設計します。単に Lighthouse を回すだけでなく、AI に差分を解析させ、PR コメントとして「何が原因の候補か」「次の一手は何か」まで返すところまで踏み込みます。
なぜ「気づいたら遅くなっていた」を許してはいけないのか
パフォーマンス劣化は、機能バグと違って「動かない」状態にはなりません。だからこそ厄介です。CI に出ない、ユーザーから報告も来にくい、けれど直帰率と離脱率は静かに悪化していきます。私は以前、ヒーロー画像を WebP から PNG に差し替えてしまった PR が誰にも気づかれずにマージされ、1 週間後に CrUX のフィールドデータでようやく気づいた、ということがありました。
この種の事故は「気をつける」では防げません。気をつけるべき点が多すぎて、人間の注意力では持ちません。CI に予算を組み込み、超えたら止める、という仕組みに落とすのが現実的な答えです。Antigravity の AI を加える理由は、数字を返すだけでは「で、どうすればいいの?」というレビューア側の負荷が残るからです。AI に差分とメトリクスを渡し、原因の仮説と修正方針まで PR コメントに書かせると、レビューが一気に動きます。
全体像 — Antigravity・Lighthouse CI・GitHub Actions の役割分担
このパイプラインは 3 つのレイヤーに分かれます。役割を最初にはっきりさせると、後で迷いません。
Lighthouse CI : Preview デプロイに対して Lighthouse を 3 回走らせ、メトリクスを収集します。中央値を採用してフレーキネス(揺れ)を抑える役目です。
Performance Budget : しきい値の集合体です。lighthouserc.js に「LCP は 2,500ms 以内」「Total Blocking Time は 200ms 以内」のように宣言します。
Antigravity Agent : 予算を超えたとき、PR の差分・Lighthouse JSON・直前 main の数値を読み取り、原因仮説と提案を PR コメントとして書き戻します。
Lighthouse は判定 AI は説明と提案 という分業がポイントです。AI に判定までさせると非決定的になり、CI の信頼性が落ちます。
Step 1: パフォーマンス予算の決め方 — 「現実的な敷地」から始める
最初の難所は予算の決め方です。理想値(LCP < 2,500ms など)を最初から採用すると、ほとんどの PR で赤になり、しばらくすると誰も見なくなります。私が個人開発で採用しているのは「現状の中央値を 5% だけ厳しくする」というルールです。
lighthouserc.js の最小構成はこうなります。
// lighthouserc.js — Performance Budget の宣言
// このファイルは Lighthouse CI が読み込み、しきい値超えで非ゼロ終了します
module . exports = {
ci: {
collect: {
// Vercel Preview などのプレビュー URL を環境変数で渡す
url: [process.env. PREVIEW_URL ],
numberOfRuns: 3 , // 3 回走らせて中央値を採用(フレーキネス対策)
settings: {
preset: "desktop" , // mobile も別ジョブで回すのがおすすめ
throttlingMethod: "simulate" ,
},
},
assert: {
assertions: {
// 数値は「現状中央値 × 1.05」を初期値にする運用
"categories:performance" : [ "error" , { minScore: 0.85 }],
"largest-contentful-paint" : [ "warn" , { maxNumericValue: 2500 }],
"total-blocking-time" : [ "error" , { maxNumericValue: 300 }],
"cumulative-layout-shift" : [ "error" , { maxNumericValue: 0.1 }],
// バンドルサイズの肥大化は CLS や TBT より早く現れる先行指標
"resource-summary:script:size" : [ "warn" , { maxNumericValue: 350000 }],
},
},
upload: {
target: "temporary-public-storage" , // 結果 URL を PR にコメントできる
},
},
};
期待される動作は、「Performance スコア 0.85 を下回る、または TBT が 300ms を超える PR は CI が赤になる」「LCP は警告のみで赤にしない」「結果 URL は PR コメントに自動で貼られる」の 3 点です。error と warn を使い分けると、最初から全員を止めずに「育てて」いけます。
なぜ error ではなく warn を混ぜるのか。理由は、運用初期に予算を厳しくしすぎると「Lighthouse CI がうるさいから無視するボット」を脳内に作ってしまうからです。これが起きるとパイプラインは死んだも同然なので、最初は warn で見せて、納得感が生まれてから error に格上げします。
Step 2: GitHub Actions ワークフロー — Preview を待ってから測る
Vercel・Cloudflare Pages・Netlify などのプレビューデプロイが終わってから Lighthouse を走らせるのが定石です。デプロイ完了イベントを待たずに測ると、ビルドが間に合わず 404 を Lighthouse が拾うことがあります。
# .github/workflows/lighthouse-ci.yml
name : Lighthouse CI
on :
pull_request :
types : [ opened , synchronize , reopened ]
jobs :
lhci :
runs-on : ubuntu-latest
# Preview デプロイの完了を待つ。Vercel の場合は deployment_status イベントを使うのが確実
if : github.event.pull_request.head.repo.fork == false
steps :
- uses : actions/checkout@v4
- name : Wait for Preview Deployment
id : wait
uses : patrickedqvist/wait-for-vercel-preview@v1.3.1
with :
token : ${{ secrets.GITHUB_TOKEN }}
max_timeout : 600 # 最大 10 分待つ
environment : Preview
- uses : actions/setup-node@v4
with :
node-version : 20
- name : Run Lighthouse CI
env :
PREVIEW_URL : ${{ steps.wait.outputs.url }}
run : |
npm install -g @lhci/cli@0.13.x
lhci autorun || echo "LHCI_FAILED=true" >> $GITHUB_ENV
- name : Upload report
uses : actions/upload-artifact@v4
with :
name : lhci-report
path : .lighthouseci/
retention-days : 14
- name : Trigger Antigravity AI Analysis
if : env.LHCI_FAILED == 'true'
# Step 5 で詳しく見る AI 解析ジョブを呼び出す
run : gh workflow run antigravity-perf-analysis.yml -F pr=${{ github.event.number }}
env :
GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
このワークフローのポイントは、Lighthouse CI が失敗しても || echo で握りつぶさず LHCI_FAILED=true を環境変数に書き出していることです。失敗した「事実」は捨てずに保持し、その後の AI 解析ジョブを起動するトリガーに使います。失敗時の終了コードでパイプライン全体を即殺すると、AI 解析が走らずレビューアが「数値が下がった」とだけ知らされる地獄になります。
Step 3: 直前 main のスナップショットと差分を取る
「数値が悪い」だけでは原因が分かりません。AI に「悪くなったコミット」を答えさせるには、PR の数値 と 直前 main の数値 の両方が必要です。
#!/usr/bin/env bash
# scripts/lhci-diff.sh — PR と main の Lighthouse JSON を比較して差分テーブルを生成
set -euo pipefail
PR_REPORT = ".lighthouseci/lhr-pr.json"
MAIN_REPORT = ".lighthouseci/lhr-main.json"
if [ ! -f " $PR_REPORT " ] || [ ! -f " $MAIN_REPORT " ]; then
echo "❌ 必要な JSON が揃っていません" >&2
exit 1
fi
# jq で 4 指標を取り出してテーブルにする
node -e '
const pr = require("./.lighthouseci/lhr-pr.json");
const base = require("./.lighthouseci/lhr-main.json");
const k = ["largest-contentful-paint","total-blocking-time","cumulative-layout-shift","speed-index"];
const fmt = n => typeof n === "number" ? n.toFixed(0) : n;
console.log("metric,base,pr,delta");
for (const id of k) {
const a = base.audits[id].numericValue;
const b = pr.audits[id].numericValue;
console.log(`${id},${fmt(a)},${fmt(b)},${fmt(b - a)}`);
}
' > .lighthouseci/diff.csv
cat .lighthouseci/diff.csv
scripts/lhci-diff.sh の期待出力は次のようになります。
metric,base,pr,delta
largest-contentful-paint,1820,2640,820
total-blocking-time,140,310,170
cumulative-layout-shift,0,0,0
speed-index,1900,2400,500
この CSV を AI に渡せば、メトリクスの絶対値ではなく「悪化幅」をベースに考えさせることができます。なぜ差分にこだわるのか。絶対値だけだと「もともと遅いのか」「この PR で遅くなったのか」を AI が混同し、無関係なファイルを犯人扱いし始めるからです。差分を渡すだけで仮説の質が体感で 2 段階上がります。
Step 4: Antigravity AI に差分を解析させる
ここが本記事の主役です。Antigravity の Agent SDK(あるいは GitHub Actions 内で gemini-3-pro を CLI 経由で呼ぶ実装でも構いません)に、差分 CSV PR の変更ファイル一覧 package.json の差分 を渡して、原因の仮説を立てさせます。
// scripts/perf-explain.mjs — AI に劣化原因を仮説立てさせる
// CI から呼び出され、PR コメント用の Markdown を標準出力に書き出します
import fs from "node:fs/promises" ;
import { runAgent } from "@google/antigravity-agent" ; // 仮: Agent SDK を CLI ラッパーで利用
const diffCsv = await fs. readFile ( ".lighthouseci/diff.csv" , "utf8" );
const changedFiles = await fs. readFile ( ".changed-files.txt" , "utf8" );
const pkgDiff = await fs. readFile ( ".pkg-diff.txt" , "utf8" ). catch (() => "" );
const prompt = `
あなたは Web Performance のエキスパートです。以下の情報から、PR で何が原因で
パフォーマンスが悪化した可能性が高いかを「仮説」として最大 3 つ示してください。
推測には必ず差分 CSV の数値か、変更ファイル名のどれかを根拠として明記すること。
推測の根拠が薄いものは出力しないでください。
# Lighthouse 差分(main → PR)
${ diffCsv }
# 変更ファイル一覧
${ changedFiles }
# package.json の差分(依存関係の変化)
${ pkgDiff || "(変更なし)"}
# 出力形式
- 仮説 1: ...
- 根拠: ...
- 次の一手: ...
- 仮説 2: ...
(最大 3 件まで。1 件しか自信がなければ 1 件で構いません)
` ;
const result = await runAgent ({
model: "gemini-3-pro" ,
prompt,
temperature: 0.2 , // 推論はあまりブレさせない
maxOutputTokens: 1200 , // PR コメントとして長すぎないサイズに抑える
});
// 失敗時は黙らずに stderr に書く(CI ログに残す)
if ( ! result.text) {
console. error ( "❌ AI 応答が空でした" );
process. exit ( 1 );
}
console. log (result.text);
このスクリプトの期待出力(PR コメントになる Markdown)はこんな雰囲気になります。
- 仮説 1: ヒーロー画像の差し替えで初回画像が大きくなった可能性
- 根拠: LCP が +820ms 悪化、変更ファイルに `public/hero.png` が含まれる
- 次の一手: WebP / AVIF への変換と `loading="eager"` の維持を確認
- 仮説 2: クライアント JS が肥大化した可能性
- 根拠: TBT が +170ms、 `package.json` で `chart.js` が追加されている
- 次の一手: 動的 import への切り出し、または未使用機能の削除
temperature: 0.2 にしているのは、CI で動かす推論に派手な創造性は要らないからです。むしろ毎回似た結論に収束してくれた方が、レビューアは過去の経験と紐付けて読めます。maxOutputTokens: 1200 はトークン爆発を防ぐためです。Step 7 で詳しく扱います。
Step 5: PR コメント設計 — 数値だけでなく「次の一手」を返す
ここはコードよりも設計の話が大事です。AI コメントを「鬱陶しいボット」にしないために、私は次の 3 ルールを徹底しています。
1 PR につきコメントは 1 件、上書き編集する : GitHub の gh pr comment --edit-last あるいは Octokit の issues.updateComment で既存コメントを更新します。push のたびに新規コメントを積み増すと、レビュー画面が AI で埋まり人間のコメントが流されます。
必ず 数値テーブル → 仮説 → 次の一手 の順で書く : 数値が一番上にないと、人間がスクロールして読まなくなります。
しきい値超え以外のメトリクスはコメントしない : 「LCP は OK でした」のような肯定情報は CI のステータスバッジで十分です。AI はネガティブ情報だけを語る方が信用されます。
PR コメント本体のテンプレートはこんな構造です。
## ⚠️ Lighthouse Performance Regression Detected
| 指標 | main | この PR | 差分 |
|---|---|---|---|
| LCP (ms) | 1820 | 2640 | **+820** |
| TBT (ms) | 140 | 310 | **+170** |
### 🔍 AI による原因仮説(2 件)
(ここに `perf-explain.mjs` の出力が入る)
---
<sub>このコメントは Antigravity AI が自動生成しています。 `/perf rerun` で再実行できます。</sub>
末尾の /perf rerun のような「対話の入り口」を残しておくと、レビューアが AI の判断を疑ったとき再実行しやすく、信頼が生まれます。CI を「壊れたら止める番人」だけにせず「対話できる相棒」にする、という思想です。
Step 6: 段階的ブロッキング戦略 — 警告から必須・ブロックへ育てる
予算は最初から「マージブロッカー」にしてはいけません。私は次の 3 段階で運用しています。
第 1 段階(最初の 2〜4 週間) : すべて warn。AI コメントだけ流す。チームに「数字が悪化したらコメントが付く」という体験を持ってもらう期間です。
第 2 段階(その後 1〜2 ヶ月) : TBT と CLS だけ error に格上げ。ユーザー体感への影響が決定的なメトリクスから順番に必須化します。
第 3 段階 : Performance スコア全体の error 化。例外を出すための「3 営業日以内に返済する技術的負債チケット」運用を同時に始めます。
なぜ段階的にするのか。一気にブロックにすると、レビュー文化が育つ前に「予算を回避するためのオプトアウトラベル」が乱用されるからです。私が見てきた限り、最初の 1 ヶ月で「ボットの存在に慣れる」フェーズを設けると、その後の必須化がはるかにスムーズに進みます。
Step 7: コストとレート制限 — トークン爆発を防ぐ実装パターン
AI を CI に組み込むときに必ず考えるべきはコストです。PR が荒れるリポジトリだと、月数千ドルの請求が突然降ってきて慌てることになります。私が採用している防御策は次の通りです。
PR ごとに 1 日 5 回まで : GitHub Actions の concurrency グループと、簡単な KV ストア(GitHub Variables でも代用可)でカウントします。
差分が小さい PR では AI を起動しない : 変更ファイル数が 3 未満なら、AI 解析をスキップして数値テーブルのみ投稿します。「typo 修正で AI が走った」みたいな無駄打ちを抑制できます。
AI 出力に maxOutputTokens を必ず設定する : 1,200〜1,500 トークンが PR コメントとして読みやすい上限です。
モデルは flash 系を第一選択にする : 推論が重くないので Gemini Flash で十分機能します。pro を使うのは「Flash で原因が当たらなかったとき」の二段ロケットだけにします。
// scripts/should-run-ai.mjs — AI 解析を起動すべきかを判定
import { execSync } from "node:child_process" ;
const baseSha = process.env. BASE_SHA ;
const prSha = process.env. PR_SHA ;
const changedFiles = execSync ( `git diff --name-only ${ baseSha } ${ prSha }` )
. toString ()
. trim ()
. split ( " \n " )
. filter (Boolean);
// しきい値: 変更ファイル数が 3 未満なら AI を呼ばない
if (changedFiles. length < 3 ) {
console. log ( "SKIP_AI=true" );
process. exit ( 0 );
}
// しきい値: 静的アセット(画像・font)だけの変更なら呼ぶ価値あり
const onlyAssets = changedFiles. every ( f => / \. (png | jpe ? g | webp | avif | woff2 ? ) $ / . test (f));
if (onlyAssets) {
console. log ( "RUN_AI=true REASON=assets-only" );
process. exit ( 0 );
}
console. log ( "RUN_AI=true" );
このスクリプトを Step 2 のワークフロー冒頭で呼び出し、SKIP_AI=true なら AI 解析ジョブをスキップする条件分岐を入れます。maxOutputTokens の設定と合わせて、月のコストを実測でだいたい 1/4 に抑えられました。
Step 8: 本番モニタリングへ閉じる — RUM データを予算に戻す
ここまでのパイプラインは閉じています。Lighthouse が測り、AI が説明し、人間がレビューします。ですが Lighthouse はラボデータです。半年も運用していると、ラボの数値と実ユーザー体感の数値はじわじわとズレていきます。「CI は緑なのにユーザーから遅いと言われる」状態が起きたら、予算が現実から離れたサインです。
対処は単純で、Real User Monitoring(RUM)の数値を予算に戻す週次ジョブを動かします。CrUX や Sentry / SpeedCurve / DebugBear など使っている RUM プロバイダから p75 を取得し、lighthouserc.js の閾値を更新する PR を自動で作るだけです。PR は人間がレビューしてマージします。予算は契約なので、契約変更には人間の署名を残しておくべきです。
// scripts/sync-budget-from-rum.mjs — RUM の数値で予算を更新する週次ジョブ
import fs from "node:fs/promises" ;
// 直近 7 日間のフィールドデータを取得(API は使用中の RUM に置き換え)
const fieldData = await fetch ( "https://api.example-rum.com/web-vitals?days=7" , {
headers: { Authorization: `Bearer ${ process . env . RUM_TOKEN }` },
}). then ( r => r. json ());
const fieldLcp = fieldData.percentiles.p75.lcp; // CrUX の標準も p75
const fieldTbt = fieldData.percentiles.p75.tbt;
// 現状値より 5% だけ厳しい値を提案する
const proposed = {
lcpBudget: Math. round (fieldLcp * 0.95 ),
tbtBudget: Math. round (fieldTbt * 0.95 ),
};
await fs. writeFile ( "lighthouserc.json" , JSON . stringify (proposed, null , 2 ));
console. log ( "✅ Proposed new budget:" , proposed);
このジョブを GitHub Actions の schedule で週次実行し、chore(perf): refresh budget from RUM (week of YYYY-MM-DD) という PR を作ります。マージは人間判断にするのがポイントです。これがあるかどうかで、予算が「現実に追従して育つ」のか「初期値のまま朽ちる」のかが分かれます。
よくある落とし穴
実際に運用してみて踏んだ罠をいくつか共有します。
1. プレビューデプロイのキャッシュで数値が良くなりすぎる
Vercel などのプレビューはエッジキャッシュが乗りやすく、Lighthouse が「ありえないほど速い」結果を返すことがあります。?_v=${PR_SHA} のようなクエリを Lighthouse の URL に付けてキャッシュバストするのが確実です。私はこれに気づくまで「なぜか PR の方が main より速い」現象に 1 週間悩みました。
2. CI 環境のネットワーク変動でフレーキネスが出る
numberOfRuns: 3 の中央値で抑えていますが、それでも CI ランナーの混雑で 100ms 単位のブレが出ます。しきい値は「5% の余裕」を持たせ、ピンポイントで攻めない方が長期運用しやすいです。
3. AI が無関係なファイルを犯人扱いする
差分 CSV を渡さず変更ファイル一覧だけ渡すと、AI は「README.md を変更したから遅くなった」のような暴論を平気で出します。Step 3 の差分 CSV は省略しないでください。
4. PR コメントが多重投稿される
comment と update-comment を混在させた結果、PR が AI コメントで埋まる事故が起きます。gh pr comment --edit-last か Octokit の更新 API に統一しましょう。
5. モバイル環境を測り忘れる
デスクトップだけだと、スマホでの体感劣化を見逃します。Lighthouse CI は preset: "mobile" のジョブを別に切るのがおすすめです。実ユーザーの 60〜70% はモバイルです。
全体を振り返って — まずは「警告だけのボット」を今週末に立ち上げる
ここまで設計と実装を一通り見てきました。最初から全部やる必要はありません。私のおすすめは「warn だけの Lighthouse CI を今週末に立てる」ことです。AI 解析もコメントも要らない、Performance スコアだけ表示するボットです。
それを 2 週間動かすと、自分のサイトの「現実的な敷地」が見えてきます。LCP の中央値、PR ごとの揺れ幅、悪化が起きやすいページが分かったところで、本記事の Step 4 以降を入れていけば、過剰設計にならずに済みます。
なお、Web パフォーマンスの基礎指標そのものについては Antigravity × Web Performance 最適化 — Core Web Vitals をAIエージェントで改善する実践ガイド が下地として役に立ちます。GitHub Actions の高度な書き方は Antigravity × GitHub Actions 高度な CI/CD パイプライン構築ガイド と、PR レビュー自動化全般は Antigravity × GitHub で Pull Request レビューを AI 自動化する実践ガイド を併せて読むと、本パイプラインを更に組み上げやすくなります。