プログレッシブ Web UI
体感パフォーマンスとは、UI を「実際に速くすること」だけでなく「速く感じさせること」です。2秒で読み込まれても、すぐにスケルトンを表示するページは、1.5秒で読み込まれるがずっと真っ白な画面を表示するページよりも速く感じられます。
ユーザーがコードを共有したとき:
- コードを読み、ユーザーが読み込み中や待機中に現在何を見ているかを理解する
- 以下の判断ガイドを使用して、該当するテクニックを特定する
- 何を変更するか、なぜそれが体感速度を向上させるか、そして具体的なコードを使ってどのように実装するかを説明する
中核原則
1. 「最初の表示」とそれ以外を分離する
常に以下を区別する:
- 最初の描画に必要なもの
- シェルが表示された後にロードできるもの
- ユーザーの操作まで待つことができるもの
すべてのデータを同じ緊急度で扱わない。
2. データより先に構造を
ページシェル(レイアウト、ナビスロット、ヘッダーフレーム)は、データフェッチが完了する前にブラウザに送信されるべき。ユーザーは最初の100~200ミリ秒で速度の印象を形成する。実際の読み込み時間が同じでも、構造をすぐに表示することで待ち時間の感じ方が軽減される。
3. クリックを無視させない
すべての操作(クリック、タップ、ナビゲーション)は、最初のフレームで目に見える応答を生成しなければならない。実際の処理に時間がかかる場合は、すぐに中間状態を表示する。
4. ローディングUIだけでなく構造を修正する
loading.tsx やスケルトンは見た目を改善するが、過剰なフェッチや逐次的なデータ依存関係は修正しない。常に根本原因に対処する:
- 逐次的な
awaitチェーン →Promise.allで並列化 - 単一の大きなフェッチ → 最初の表示用と遅延用に分割
- ナビ内での完全なプロファイルフェッチ → 軽量なナビ用IDヘルパーを使用
5. ナビゲーションも速く感じさせなければならない
体感速度は初期読み込みだけの話ではない。ページ遷移、再訪問、操作も同様に速く感じさせる必要がある:
- ユーザーがクリックする前にルートをプリフェッチ
- ナビゲーション開始時に即座に
loading.tsxを表示 - SWR/キャッシュを使用して再訪問時は瞬時に表示
判断ガイド
| 症状 | テクニック |
|---|---|
| データ読み込み中に空白画面が表示される | スケルトンスクリーン / Suspense ストリーミング |
| ボタンクリックに即座に応答がない | 楽観的UI / インスタントフィードバック |
| ページ遷移がクリック後すぐに視覚的に開始されない | loading.tsx + プリフェッチ |
| 重いコンポーネントが初期レンダリングを遅くする | 動的インポート |
| 同じデータにアクセスするたびにスピナーが表示される | SWR stale-while-revalidate |
| 1つの遅いAPIがページ全体をブロックする | Suspense ストリーミング(並列非同期Server Components) |
| Auth/プロファイルフェッチがナビやページシェルを遅らせる | Authレイアウト分離(references/auth-performance.md 参照) |
| 画像がレイアウトシフトを引き起こしたり、遅く感じる | next/image と priority / blur プレースホルダー |
| フォントスワップによりスタイル未適用テキストがちらつく | next/font |
| フォーム送信に視覚的なフィードバックがない | useFormStatus / useActionState |
| 長いリストがスクロールのカクつきやレンダリング遅延を引き起こす | 仮想スクロール(TanStack Virtual / react-window) |
| 緊急でない状態更新がユーザー入力をブロックする | 遅延レンダリングのための startTransition |
分析ワークフロー
ステップ1: ユーザーが最初に何を見るかを特定する
0~200ミリ秒で何がレンダリングされるか?それが空白かスピナーであれば、スタイル修正の前に構造変更が必要。
ステップ2: データを緊急度で分類する
- 最初の表示 — ページを意味のあるものにするために必須
- 遅延 — シェルが表示された後にストリーミングできる
- オンデマンド — ユーザー操作後にのみロード
ステップ3: データ依存関係を修正する
- 逐次
await→Promise.allまたは個別の非同期 Server Components - 初期ロードでの過剰フェッチ → API分割またはページネーション
- ナビ内の完全なプロファイル → 個別のナビ用IDヘルパー
ステップ4: プログレッシブレンダリングを適用する
必要なものに基づいて選択:
loading.tsx— インスタントナビゲーションスケルトン(ページ全体)<Suspense>— 粒度の高いストリーミング(セクションごと)useOptimistic— インスタントなミューテーションフィードバックdynamic()— 重いコンポーネントのバンドルを遅延- SWR — キャッシュからのインスタントな再訪問
ステップ5: 体感パフォーマンスを検証する
- 意味のあるものが即座に(<200ms)表示されるか?
- すべてのローディング状態でユーザーに情報が伝えられているか?
- 完全なデータが到着する前にUIは応答するか?
- 操作が最初のフレームでフィードバックを生成するか?
出力形式
このスキルを適用する際は、以下の構造で応答する:
## 現在起きていること
- [ユーザーが現在経験していることを説明]
## 根本原因
- [コード内で遅さを感じさせている原因]
## 変更内容とその理由
- [変更1]: [これが体感速度を向上させる理由]
- [変更2]: ...
## 実装
[コード]
## 注意事項
- [注意点、エッジケース、フォローアップの改善点など]
参照
完全なコード例を含む実装の詳細については、以下を参照:
references/nextjs-patterns.md— スケルトンスクリーン、楽観的UI、Suspense ストリーミング、プリフェッチ、動的インポート、SWR、画像、フォント、インタラクションフィードバック、useFormStatus、仮想スクロール、startTransitionreferences/auth-performance.md— Authレイアウト分離、ナビ用IDヘルパー、リクエスト/プロセスウォームアップ、フォールバック状態設計
特定のテクニックを実装する際は、関連する参照を読むこと。
修正すべきアンチパターン
常に以下を検出し対処する:
- トップレベルレイアウトで完全なプロファイル/authを待つ → すべてのページを遅延させる
- 独立したデータに対する逐次的な
awaitチェーン → 不要な直列化 - 根本的な過剰フェッチを修正せずに
loading.tsxを追加 - 初期ロードでデータセット全体をレンダリング → ページネーションまたは仮想化
- 操作完了までクリックに視覚的フィードバックがない → 即座に応答を
- ナビとページで異なる認証ソース → ユーザー名の不整合
- サーバー応答後にのみ管理されるフォーム送信状態 → インスタントなペンディング状態のために
useFormStatusを使用 - マウント時に長いリストのすべてのアイテムをレンダリング → 仮想化(表示行のみレンダリング)
- ユーザー入力をブロックする高コストな状態更新(フィルター/ソート/検索) →
startTransitionでラップ