Webアプリの検索画面を80%高速化した話 — 「推測するな、計測せよ」を実践して
Photo by Mike van den Bos on Unsplash
Webアプリケーションの検索機能で、ボタンを押してから結果が表示されるまで約10秒かかっていました。ユーザーにとって10秒は非常に長く、体験を大きく損ねるレベルです。
今回、この検索結果ページのパフォーマンス改善に取り組み、最終的に約2.2秒まで短縮(約80%の改善)することができました。この記事では、どのようなアプローチで改善を進めたかをまとめます。改善アプローチの全体像
今回の改善は「① 計測基盤の整備 → ② ボトルネック特定 → ③ 優先度順に改善 → ④ 効果検証」という4ステップで進めました。
パフォーマンス改善でまず取り組んだのは、計測基盤の整備です。「推測するな、計測せよ」という有名な原則に従い、いきなりコードを書き換えるのではなく、どこがボトルネックなのかを正確に把握することから始めました。
具体的には、サーバーサイドのデータ取得時間、クライアントサイドのデータベースDuckDB(WASM)初期化時間、検索パイプライン全体の各ステップの所要時間、ページ遷移をまたぐエンドツーエンドの処理時間を計測できる仕組みを作りました。
この計測基盤によって、全体の約50%を占める巨大なボトルネックの存在が明らかになりました。
ボトルネックの正体
計測の結果、処理時間の内訳は以下のようになっていました。
・サーバーデータ取得:5,160ms(全体の約50%)
・外部APIからのデータ取得:2,100ms(約21%)
・WASM初期化:700ms(約7%)
・重複処理(6回実行):530ms(約5%)
・クエリ実行:300ms(約3%)
・データ変換:170ms(約2%)
最大のボトルネックは、サーバーサイドで複数のデータ取得をPromise.allでまとめて待っていた部分でした。ページ表示に必須ではない重いデータ取得が、ページ全体のレンダリングをブロックしていたのです。
改善1: Streaming SSRの導入(最大の成果)
最も効果が大きかったのが、Next.jsのStreaming SSRを活用した改善です。
改善前は、サーバーサイドですべてのデータ取得が完了するまで、ユーザーには真っ白な画面が表示され続けていました。約5秒間、何も見えない状態です。
改善後は、重いデータ取得をSuspenseと非同期サーバーコンポーネントで分離しました。地図などの主要UIは先に表示され、補助的なデータは後から非同期でストリーミングされます。
結果:
・TTFB(最初のバイトが届くまでの時間):4,320ms → 329ms(92%削減)
・FCP(最初のコンテンツ表示):4,680ms → 約700ms(85%削減)
たった1つのアーキテクチャ変更で、サーバーサイドのボトルネックをほぼ解消できました。重いデータ取得がある場合、Streaming SSRは最もコスパの高い最適化手法だと実感しました。
改善2: WASMランタイムのプリフェッチ
このアプリケーションでは、クライアントサイドでDuckDB(WASM版)を使ってデータ処理を行っています。検索の初回実行時にWASMバイナリのロードで約700msかかっていました。
そこで、アプリ起動時にバックグラウンドでWASMの初期化を済ませておくプリフェッチを導入しました。ユーザーが検索ボタンを押す時点ではすでに初期化が完了しているため、この700msを完全に排除できました。
結果:
・WASM初期化:700ms → 0ms(100%削減)
・検索パイプライン全体:約19%短縮
改善3(進行中): 重複処理の排除
計測で発見したもう一つの無駄は、テーブル存在確認の処理が1回のページ表示で6回も重複実行されていたことです。サイドバーとマップのそれぞれで3回ずつ、合計約530msの無駄が発生していました。
原因はuseEffectの依存配列にある状態値がマウント後に変化し、再実行が発生するためです。キャッシュによる排除を試みていますが、Next.js + Turbopackの環境ではモジュールレベル変数がチャンク間で共有されないという挙動があり、現在も解決策を模索中です。
改善結果まとめ
・Streaming SSR:-4,830ms(TTFB 92%削減。最大の成果)
・WASMプリフェッチ:-700ms(初期化コスト100%排除)
・重複処理の排除(目標):-530ms(6回→1回に削減)
・合計:約-6,060ms
技術的な学び
Streaming SSRの威力
重いデータ取得をSuspenseで分離するだけで、TTFBを92%削減できたのは衝撃的でした。SSRでレンダリングブロックが発生している場合、まず最初に検討すべき手法です。
Turbopack環境の落とし穴
Next.js 15 + Turbopack環境では、モジュールレベルの変数が異なるチャンク間で共有されないことがあります。globalThisを使っても回避できないケースがあり、キャッシュ戦略を考える際は注意が必要です。
計測ファーストのアプローチ
計測基盤を先に整備したことで、改善の優先順位付けと効果検証がスムーズに進みました。体感や推測ではなく、数値に基づいて判断できたことが、短期間で大きな成果につながった要因です。
おわりに
パフォーマンス改善は地道な作業ですが、ユーザー体験に直結する重要な取り組みです。今回の経験を通じて、「まず計測し、最大のボトルネックから潰す」というシンプルな原則の有効性を改めて実感しました。
今後も、まだ残っている改善ポイント(重複処理の排除、ページ統合による遷移コスト削減、楽観的UIの導入など)に取り組んでいく予定です。