管理画面が30秒→5秒になった話 — 4つのボトルネックが連鎖していた
本番の管理画面が、開くたびに30秒以上かかっていた。
「本番だけ遅い」という現象は、ローカルでは再現しない分だけタチが悪い。ログを眺めながら「なんでだ」とつぶやくこと数時間。結果的に原因は4つの連鎖で、1日で5倍以上の高速化に成功した。その過程を記録しておく。
問題:「30秒」という数字が示すもの
PiloTube(パイロチューブ)の管理画面は、チャンネルや動画のデータを一覧表示する。ローカル環境では2〜3秒で表示されていたのに、本番にデプロイした途端に様子がおかしくなった。
最初は「本番サーバーが重いだけ」と思っていた。正直、甘く見ていた。
でも30秒というのは「ちょっと重い」じゃない。ユーザーが離脱するレベルだし、自分でも使っていてストレスが溜まる。何より「なぜローカルと差があるのか」がわからないまま放置するのは気持ち悪い。
計測ツールを仕込んで、どこで時間が消えているのかを洗い出すことにした。
気づき:ログを見たら「外部API呼び出し」が並んでいた
まず処理の各ステップに計測ログを入れた。すると、外部APIへのリクエストがシリアル(直列)で走っていることがわかった。
具体的にはこういう構造になっていた。
1件目のチャンネルデータを取得 → 完了を待つ
2件目のチャンネルデータを取得 → 完了を待つ
3件目のチャンネルデータを取得 → 完了を待つ
...(n件目まで繰り返す)
チャンネルが10件あれば、APIレスポンスが1件あたり2〜3秒かかるとして、単純に掛け算で20〜30秒。「30秒」の正体はこれだった。
ローカルが速かった理由もわかった。ローカルはモックデータを使っていたので、外部APIを叩いていなかったのだ。本番だけ遅い理由が、ここでやっと腑に落ちた。
課題:直列を並列にしたら、今度は別の問題が出た
「じゃあ Promise.all で並列化すればいい」と思って実装した。確かに速くなった。でも、速くなったのに、まだ重いという謎の状態になった。
並列化後でも8〜10秒かかっている。外部API呼び出しが並列になったなら、理論上は2〜3秒になるはずなのに、なぜ倍以上かかるのか。
ここで2つ目の問題が見えてきた。
async関数の中に、重い同期処理が混入していた。
コードを追うと、データ取得後の「加工処理」の中に、ループ内で大量の文字列操作をしている箇所があった。これが event loop(JavaScriptの処理キュー)をブロックしていた。
JavaScriptはシングルスレッドなので、重い同期処理が走ると、他の非同期処理が「順番待ち」になる。せっかく並列で外部APIを叩いても、データが返ってきた後の処理が詰まっていたわけだ。
実践:4つのボトルネックを1つずつ剥がした
問題を整理すると、こういう連鎖になっていた。
- 外部API呼び出しが直列 → 全体の待ち時間がn倍になる
- データ加工処理が同期ブロック → event loopが詰まる
- 不要なデータまで取得している → APIレスポンスが無駄に大きい
- 同じデータを何度も取得している → キャッシュなし
順番に対処した。
① 外部API呼び出しを並列化
Promise.all でまとめて投げるように書き直した。
// Before: 直列
for (const channel of channels) {
const data = await fetchChannelData(channel.id);
results.push(data);
}
// After: 並列
const results = await Promise.all(
channels.map(channel => fetchChannelData(channel.id))
);
これだけで30秒 → 約10秒になった。
② 重い同期処理を分割・軽量化
データ加工のループ処理を見直した。不要な文字列操作を削除し、どうしても必要な処理は setImmediate を使って細かく分割することで、event loopを占有しないようにした。
③ APIのフィールド指定(必要なデータだけ取る)
外部APIのリクエストに fields パラメータを追加して、表示に必要なフィールドだけを取得するようにした。レスポンスのサイズが約60%削減された。
④ 短期キャッシュの導入
管理画面は1分以内に何度も開くことがある。同じデータを毎回APIから取得するのは無駄なので、60秒のインメモリキャッシュを入れた。2回目以降のアクセスはキャッシュから返るので、ほぼ瞬時に表示される。
結果:30秒 → 5秒以下
4つの対処をすべて適用した結果がこれ。
| 状態 | 表示時間 | |------|----------| | 修正前 | 30秒以上 | | ①並列化のみ | 約10秒 | | ①②同期処理改善 | 約7秒 | | ①②③フィールド指定 | 約6秒 | | ①②③④キャッシュ導入 | 5秒以下 |
初回アクセスは5秒、2回目以降は1秒以内。体感がまったく変わった。
正直、最初に「直列APIが原因」とわかった時点で「これで終わりだ」と思っていた。でも並列化しただけでは半分しか解決しなかった。ボトルネックは重なっていた。
教訓:「遅い」には必ず複数の理由がある
今回の経験から持ち帰ったことを整理しておく。
1. 「本番だけ遅い」はモックとの差を疑え
ローカルがモックで動いているなら、本番で初めて発覚するボトルネックがある。本番に近い環境でのテストは、やっぱり必要だと痛感した。
2. 1つ直しても終わりじゃない
今回は4つのボトルネックが連鎖していた。1つ直して「速くなったけどまだ重い」という状態になったとき、そこで止まらないことが大事だった。計測を続けて、次の壁を探す。
3. async/await は「並列」じゃない
await を使えば非同期処理ができると思っていても、ループの中で await を書いたら直列になる。これは初歩的なミスだけど、本番のデータ量になって初めて顕在化することがある。
4. event loop ブロックは見落としやすい
非同期処理を並列化しても、同期処理が重ければ詰まる。「非同期にした」と「event loopをブロックしていない」は別の話。この区別は意識的に持っておく必要がある。
PiloTube はまだ開発途中で、こういう「本番で初めてわかる問題」がこれからも出てくると思う。でも1日でここまで改善できたのは、ちゃんとログを取って、1つずつ仮説を検証したからだった。
「遅い」を「なんとなく重い」で終わらせないこと。それだけで、解決できる問題はずっと増える。
チャプター生成AI
URL貼るだけ。AIがチャプターを自動生成。
YouTubeのURLをコピーして貼る
「生成する」を押す
概要欄にコピペして完了
月3回まで無料 · クレジットカード不要