前提
個人開発のEDINET分析ツール(財務レポートを解析してDiscordに投げる趣味プロジェクト)で、フロントエンドのコードをAIにほぼ丸投げして書かせた部分があった。
後から見直していたら、明らかに厳しい実装が出てきたので調査した記録。今回は原因の特定と応急処置(SELECT修正)までで、DBスキーマ自体の見直しは今後の課題として持ち越す。
出てきたコード
API利用状況を表示するダッシュボードコンポーネントで、以下のように4本のAPIをPromise.allで並列取得していた。
const [statsRes, logsRes, tsRes, statusRes] = await Promise.all([
apiClient.get(`/api-logs/stats?days=${days}`),
apiClient.get('/api-logs/logs?limit=50'),
apiClient.get(`/api-logs/timeseries?days=${days}&interval=${interval}`),
apiClient.get(`/api-logs/status?days=${days}`),
])
取得後、4つのstateをfinallyブロックで一括更新し、loadingも1つのフラグで管理していた。
最初の疑問:Promise.allの並列度は妥当か
Promise.all自体は4本のリクエストを同時発火するので、直列でawaitを重ねる実装(MDN: Promise.all)と比べれば理にかなっている。
ただし設計上の課題は残る。
loadingが1つのフラグなので、一番遅いレスポンスに画面全体の表示タイミングが引っ張られるuseEffectの依存配列が[days, interval]のみで、intervalしか変えていない操作でも無関係なlogsまで再取得される
対策案としては、各エンドポイントを個別のuseEffect+個別stateに分割し、届いた順にレンダリングする方式が考えられる。この場合、依存配列も自然に分離でき、interval変更時に不要な再取得も防げる。ただしこれは「体感速度」の改善であり、根本的なネットワーク・サーバー負荷の解消にはならない。
実測結果
Chrome DevToolsのNetworkタブで実測したところ、以下の結果になった。
| エンドポイント | サイズ | レスポンスタイム(1回目) | レスポンスタイム(2回目) |
|---|---|---|---|
stats?days=7 |
0.3kB | 5.69秒 | 6.25秒 |
logs?limit=50 |
7.7kB | 200ms | 351ms |
timeseries?days=7&interval=hour |
14.5kB | 11.52秒 | 6.01秒 |
status?days=7 |
0.5kB | 7.92秒 | 11.58秒 |
同一アプリの別画面(Stats.tsx)にあるsummary・alerts・ranking系のエンドポイントは、同程度の小さいペイロードで30〜111msと桁違いに速い。
結論:問題はフロントの並列化ではなくバックエンド側
数百バイト〜十数キロバイトのレスポンスに対して秒単位〜十数秒のレイテンシは、ネットワークやReact側の描画方式では説明がつかない差だった。並列fetch自体は問題ではなく、疑うべきは以下。
- 集計クエリのN+1(プロバイダーごとにループでクエリを投げている可能性)
- 日時カラムへのインデックス未設定(
daysによるWHERE句がフルスキャンになっている可能性) interval=hourの集計処理(DB側の集計かアプリ側ループかで原因が分岐)- 2回目の計測でむしろ悪化(
stats5.69→6.25秒、status7.92→11.58秒)していることから、キャッシュが効いていない、あるいはテーブルの成長に伴って劣化する設計になっている可能性
N+1問題自体についてはN+1 query problem (MDN Glossary)を参照。
今回の対応範囲
今回は該当エンドポイントのSQLクエリを見直し、集計方法をSELECT文レベルで修正するところまでを実施した。
DBスキーマ自体(インデックス追加や正規化の見直しなど)には手を付けていない。テーブルが育つほど劣化する可能性が実測から示唆されているため、これは今後の課題として残す。
教訓
- AIに丸投げしたコードは、動いているように見えても「並列化されているか」程度の表面的なチェックだけでは不十分
- フロントエンドの実装が妥当でも、DevToolsのNetworkタブで実測しない限りボトルネックの所在は特定できない
- 「並列処理にしたから速いはず」という思い込みは、実測前の仮説に過ぎない