5/13 ローンチ予定!
PiloTube

PiloTube 開発日誌

← 「ひとり社長のAI開発記」一覧へ

並列化したのに遅い
JavaScriptのシングルスレッドを舐めてた

約6分で読めます

管理画面が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つずつ剥がした

問題を整理すると、こういう連鎖になっていた。

  1. 外部API呼び出しが直列 → 全体の待ち時間がn倍になる
  2. データ加工処理が同期ブロック → event loopが詰まる
  3. 不要なデータまで取得している → APIレスポンスが無駄に大きい
  4. 同じデータを何度も取得している → キャッシュなし

順番に対処した。

① 外部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がチャプターを自動生成。

1

YouTubeのURLをコピーして貼る

2

「生成する」を押す

3

概要欄にコピペして完了

無料でチャプターを生成する →

月3回まで無料 · クレジットカード不要