バイブコーディングした管理画面が遅い。クエリを半分にしても体感は変わらなかった
AIに指示を出して、社内向けの管理画面を作りました。ちゃんと動きます。手元では一覧が一瞬で出ます。本番だけ、開くたびに数秒なにも出ません。
そこで測って、いちばん重いクエリを 375ms から 169ms に縮めました。体感はまるで変わりませんでした。 結論から言うと、真因はコードの中にありませんでした。誰も触っていない既定値です。AIは動くコードを書きますが、既定値は直しません。指示していないからです。
「動いてはいるけど遅い」で止まっている人の時短になれば嬉しいです。
つくっていたもの
受発注まわりの社内システムです。マスタを登録し、イベントごとに原価を集計し、発注書を出します。画面は 30 ルートほどあり、ほぼすべてが一覧と編集です。
- アプリ: Next.js 16(App Router)on Vercel
- データ: Supabase(Postgres)+ Prisma
- 書いた人: ほぼAI
要件を伝えて、出てきたコードを読んで、直して、マージする。いわゆるバイブコーディングです。動くものは驚くほど速く出てきました。
そして最初の報告は、具体的ではありませんでした。「少し重たいです、全体的に」です。
なぜ「あるある」なのか
バイブコーディングは、動くところまでがとても速いです。そして動いた時点で、完成に見えます。
罠はここです。速さに関わる設定は、間違っていても画面に出ません。エラーも出ません。テストも通ります。ただ遅いだけです。症状が「動かない」ではないので、直す指示が一度も出ないのです。
もうひとつあります。既定値は「いちばん速い値」ではありません。**「どこで動かしても壊れない値」**です。本来は作る側が選ぶものを、選ばないまま本番に出していました。
ただ、これに気づくのはずっと後です。このときの私は、素直にコードを疑いに行きました。
1 回目:コードの中を疑う
まずコードを読み、遅そうな場所を 8 つ挙げました。そのうち 1 つは誤診でした。
誤診:関連テーブルの数だけ往復しているはず
Prisma で関連テーブルをまとめて取ると、関連の数だけSQLが飛ぶ、と書きました。イベント詳細で 15〜18 往復している、とも書きました。
実測したら違いました。
| イベント詳細の同じ取得 | クエリ数 | 時間 |
|---|---|---|
| 指定なし(既定) | 1 | 82ms |
relationLoadStrategy: "join" |
1 | 81ms |
relationLoadStrategy: "query" |
18 | 35ms |
Prisma 7 は既定で 1 本のJOINを投げていました。私が足した指定は、完全に何もしていません。入れたプレビュー機能ごと取り消しました。
ここは既定値が正解だった例です。既定値は常に悪者、というわけではありません。だから厄介なのだと、あとで思い知ります。
なお手元では、18 往復する "query" のほうが 2 倍速く出ています。JOINで行が膨らむコストが、往復のコストを上回るためです。本番はDBが遠いので、この順位はそのまま持ち込めません。
実際に効いたもの
製品 1000 件・イベント 6 件・原価行 6000 行のデータを作って測りました。
| Before | After | |
|---|---|---|
| イベント一覧のクエリ | 375ms | 169ms |
| 製品追加ダイアログの候補取得 | 35ms | 9ms |
| 型番の突合(JSの処理) | 8.2ms | 0.3ms |
中身は地味です。全部の列を取っていたのを、使う列だけに絞る。ダイアログの候補のために製品マスタを全件引いていたのをやめ、4 列だけの専用クエリにする。二重ループを Map に置き換える。
数字はきれいに出ました。この時点では、勝ったと思っていました。
画像は、毎回ダウンロードし直されていた
表示側でも 1 つ見つけました。こちらは素直に効いた実感があります。
画像の署名付きURLは、発行のたびにトークンが変わります。 URLが変われば、ブラウザから見ると別の画像です。つまり一覧を開くたびに、同じサムネイルを全部ダウンロードし直していました。
期限内は同じURLを返すようにして、2 回目以降をブラウザのキャッシュに任せます。ついでに、足りない分だけを発行しに行くのでストレージへの往復も減ります。
「なぜか画像だけ毎回もたつく」は、署名付きURLを使っていると高確率で踏みます。
やらかし:計測のつもりで、他人のDBを触った
計測のために supabase start を叩き、ポート 54322 につなぎました。そこは別のワークツリーが握っているスタックでした。気づく前に、他プロジェクトの開発用DBへ検証用の組織を作っています。
作った行を id 指定で消して片付けました。以降は使い捨てのデータベースを作って測り、最後に落としています。
原因を追うと、これも既定値でした。このプロジェクトのDBはポート 54422 です。.env.example に書いてある 54322 を、疑わずにそのまま使ったのが元でした。サンプルの値は、動く値であって、正しい値ではありません。
そして、体感は変わらなかった
クエリは実際に半分になりました。数字も残っています。それでも「全体的に重い」は、そのまま残りました。
ここで手が止まります。
測って、直して、速くなったのに、なぜ変わらないのか。
答えは単純でした。私が縮めたのは、支配項ではなかったのです。
2 回目:コードの外を疑う
きっかけは、別で触っている管理画面でした。同じ Supabase を使っているのに、明らかに速い。差を調べてほしい、という依頼が来ます。
そちらは、ブラウザから Supabase を直接呼ぶ作りでした。画面の骨組みはCDNから即座に出て、データは日本のブラウザから東京へ 1 往復です。こちらはサーバーで画面を組むので、ブラウザからサーバー、サーバーからDBと段を踏みます。
そのうえで、一度も触っていない既定値が 3 つありました。
-
サーバーが動く場所 —
vercel.jsonがなく、リージョンの指定もゼロ。つまり既定のiad1、米国東部です。Supabase は東京にありました。1 往復ごとに、太平洋を越えていたわけです - DBへの接続の張り方 — 直接接続のままでした。サーバーレスは実行のたびに新しい箱が立つので、毎回かけ直して名乗り直すところからやっています
-
キャッシュ — 有効化する設定だけ入っていて、
use cacheを grep した結果は 0 件でした。設定を入れた時点で満足していたやつがいます。私です
一覧 1 枚あたり 3 往復、1 往復が 150〜200ms なら、それだけで 500ms 以上が待ち時間で消えます。私が削った 200ms は、まるごとこの中に埋もれていました。
この 2 回目の調査と、リージョン・接続・キャッシュをどう直したかは、別記事に詳しく書いています。キャッシュに何を載せて何を載せないか、?pgbouncer=true が要るという思い込みを型定義で潰した話も、そちらにあります。
学び
1. AIは「動くもの」を書く。速いものは、頼まないと出てこない
リージョンも接続方式もキャッシュも、一度も指示していません。指示していない項目は、既定値のまま本番に出ます。 そして既定値は、動作確認では絶対に落ちません。
2. 既定値は「安全な値」であって「速い値」ではない
iad1 も直接接続も、壊れません。ただ遅いだけです。一方で relationLoadStrategy のように、既定が正解のこともあります。既定値は敵でも味方でもなく、単に「誰も選んでいない」という状態です。
3. 半分にしても体感が変わらないなら、支配項が別にある
375ms を 169ms にして、何も変わりませんでした。半分にしたのに効かない、は「そこは支配項ではない」という立派な観測結果です。 ここで喜ばずに引き返せるかどうかが分かれ目でした。
4. 「効くはず」を実測せずに入れると、効いていないことにも気づけない
relationLoadStrategy: "join" は何もしていませんでした。測っていなければ、勘違いしたまま次の判断へ進んでいます。前と後を測らない改善は、改善ではなく願望です。
5. 手元の計測は、本番の順位を保証しない
手元では 18 往復のほうが 2 倍速く出ました。DBが同じマシンにいるからです。本番はDBが遠いので、この順位は逆転します。計測環境と本番で「何が高くつくか」が違うなら、順位もそのままでは持ち込めません。
6. サンプルの値を、そのまま信じない
.env.example のポートを疑わずに使い、他プロジェクトのDBを触りました。サンプルは動く値であって、いまのプロジェクトの正しい値ではありません。 既定値を疑う話は、コードの外だけでなく手元の設定にも同じく効きます。
おわりに
バイブコーディングだと、疑う先がまずコードになります。目の前にあるのがコードだからです。しかも読めば必ず改善点は見つかるので、直せば少し速くなり、少し速くなるぶん、思い込みが延命します。
今回そこに 18 日かけました。1 回目の改善から、外を疑うまでの日数です。
AIが触っていない場所こそ、既定値のまま残ります。ダッシュボードの設定と、接続文字列と、有効化しただけの機能。もし手元のアプリがなんとなく遅いなら、コードを開く前に、そのあたりを一度見てください。
Discussion