この記事の要点
- ストリーミングとは、ページ全体の完成を待たずにできたところから順に送る仕組み。遅いデータが 1 つあっても画面全体が止まらない。
- 使い方は 2 通り。ページ単位なら
loading.tsx、部分単位ならSuspenseで包む。 Suspenseは「遅い部分を切り離す道具」。取得処理をできるだけ深い位置に押し込むほど、先に出せる範囲が広がる。- クローラーには段階送信せず、完成した HTML を返す。SEO 上の不利は基本的に生じない。
- フォールバックはスピナーよりスケルトンのほうが体感が良い。実際の画面の骨格に似せる。
サーバーでデータを取ってから HTML を返す方式には、素朴に作ると「一番遅い処理に全体が引きずられる」という弱点があります。売上ランキングの集計に 2 秒かかるなら、ヘッダーもナビゲーションも 2 秒待たされる、という状態です。ストリーミングはこれを解決します。この記事では仕組みと、実際にどこで区切るかの判断を扱います。
1何が起きているのか
ストリーミングでは、サーバーはまず「すぐ確定する部分」の HTML を送り出します。遅い部分にはいったん代替表示(フォールバック)を入れておき、データが揃った時点で続きを送ってブラウザ側が差し替えます。接続は開いたままで、複数回に分けて中身が届くイメージです。
2ページ単位で区切る — loading.tsx
最も手軽なのは、ページと同じフォルダに loading.tsx を置くことです。
// app/blog/loading.tsx
export default function Loading() {
return <div>読み込み中…</div>
}
これだけで、Next.js が page.tsx 以下を自動的に Suspense 境界で包みます。遷移した瞬間にレイアウトとフォールバックが表示され、ページの描画が終わると中身が入れ替わります。
ただし適用範囲は「layout の内側、page 以下」です。同じセグメントのレイアウト自身が遅い場合、その待ち時間はこの loading.tsx では覆えません。レイアウトが cookies() を読んだり、キャッシュされていないデータを取ったりしていると、遷移そのものがブロックされます。
3部分単位で区切る — Suspense
実務でよく使うのはこちらです。遅いコンポーネントだけを Suspense で包むと、それ以外は即座に送り出せます。
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* ここは即座に届く */}
<header>
<h1>ブログ</h1>
<p>最新の投稿はこちら。</p>
</header>
<main>
{/* 中で await している部分だけが後から届く */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
ポイントは、await しているコンポーネントを Suspense の中に入れることです。ページ本体で await してから結果を Suspense の子に渡しても意味がありません。その時点で既に待ってしまっているからです。
const data = await getData() をページの先頭で実行し、その下に Suspense を置く。ページ全体が待つので境界の意味がない。取得を行う小さなコンポーネントを作り、それを
Suspense で包む。待ちの発生箇所を境界の内側へ入れる。4遅い処理を深い位置へ押し込む
「どこで区切るか」は「どこで await するか」と同義です。上のほうで待つほど、静的に確定できる範囲が狭くなります。
たとえばレイアウトの先頭で params を分解しているコードは、その値が確定するまでレイアウト全体を描画できません。値を使う場所まで Promise のまま持ち回り、そこだけを Suspense で包めば、サイドバーや children は先に送り出せます。
import { Suspense } from 'react'
// async にしない: このレイアウトは params を await しない
export default function Layout({
children,
params,
}: LayoutProps<'/shop/[slug]'>) {
return (
<div>
<Sidebar />
<Suspense fallback={<h1>読み込み中…</h1>}>
{params.then(({ slug }) => (
<SlugHeading slug={slug} />
))}
</Suspense>
{children}
</div>
)
}
この「必要になる直前まで待たない」という発想は、cookies() や headers()、searchParams にも同じように当てはまります。
5フォールバックの作り方
フォールバックは「待っていることを伝える」だけでなく「これから何が来るか」を示せると効果的です。
| フォールバック | 体感 | 向いている場面 |
|---|---|---|
| スピナー | 待たされている感覚が残りやすい | 小さな部品、ボタン内の処理 |
| スケルトン | レイアウトが動かず、完成形を予測できる | 一覧・カード・表 |
| 実データの一部 | 最も自然。タイトルや画像だけ先に出す | 詳細ページ |
スケルトンは実際の要素とサイズを合わせるのがコツです。高さが違うと差し替え時に画面が飛び、かえって印象が悪くなります。
6クローラーと配信環境
検索エンジンのクローラーは、ユーザーエージェントによって判別され、ストリーミングではなく完成した HTML を受け取ります。フォールバックだけがインデックスされる心配は基本的にありません。
一方で、この扱いには副作用があります。ブラウザ向けには事前生成済みの部分を再利用できても、クローラー向けにはリクエスト時に全体を描画し直します。そのため、事前生成のときにしか手に入らない値に依存していると、人間には見えるのにクローラーには失敗する、という状態が起こり得ます。使うデータはリクエスト時にも取得できる状態にしておいてください。
また、自前サーバーで運用する場合はインフラ側の対応も必要です。Nginx などのリバースプロキシがレスポンスをバッファリングしていると、せっかく分割して送っても最後にまとめて届いてしまいます。バッファリングを無効化する設定については 本番ビルドとセルフホスト で扱います。
7つまずきやすいところ
Suspenseで包んだのに速くならない:境界の外側でawaitしていないか確認します。- ローディングが出ない:同期処理しかしていないコンポーネントは、
Suspenseで包んでも事前生成の段階で完了します。フォールバックは表示されません。 - 境界を作りすぎる:画面のあちこちが順にちらつくと、かえって落ち着かない印象になります。区切るのは「体感で分かるほど遅い部分」だけで十分です。
- レイアウトが遅い:セグメント直下の
loading.tsxでは覆えません。レイアウト内の取得処理をSuspenseで包むか、page側へ移します。
次は、そもそも待たなくて済むようにする側の話、つまりキャッシュに進みます。
子ページはありません
人気ページ
- 1 Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法
- 2 tomcat の起動 / 停止ログと catalina.log・catalina.out の違い
- 3 JavaScript で base URL を取得する方法|window.location.origin
- 4 YouTube Data API v3 エラー一覧|403・400・404 の原因と対処
- 5 Laravel エラー一覧|500/Blade/DB 接続/ルーティングの代表エラー
- 6 3Dグラフィックスのソフト一覧|Blender・Metasequoiaと用途別の選び方
- 7 Spring Frameworkのアノテーション一覧
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- Visual Studio「デザイナーを表示できません」エラーの原因と対処 2026-10-28 12:06:00
- SSH 認証に失敗しました。再試行してください の対処 2026-10-28 12:06:00
- SSH2 秘密鍵の読み込みに失敗しました の原因と対処 2026-10-28 12:06:00
- TeraTerm のよくあるエラーと対処法完全ガイド — Connection refused / 2026-10-28 12:06:00
- Visual Studio エラー一覧|コンパイルエラー・警告の確認方法と対処 2026-10-28 11:39:57
- Whitelabel Error Page の原因と消し方|Spring Boot のエラーページ 2026-10-28 11:39:57
- Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法 2026-10-28 11:39:57
- SQL WHERE 句完全ガイド 2026-10-28 11:39:57
- SQL JOIN 完全ガイド (INNER/LEFT/RIGHT/FULL OUTER) 2026-10-28 11:39:57
- tomcat の起動 / 停止ログと catalina.log・catalina.out の違い 2026-10-28 10:24:23
- Tomcat の起動確認方法 5 つ|localhost:8080・ポート・プロセス・ログ 2026-10-28 10:24:23
- Oracle 19c COMMITの使い方: 暗黙コミット・自動確定・応答不明時の確認 2026-10-28 09:59:09
- Oracle 19c DELETE文|削除対象の確認・COMMIT前の判断・ROLLBACKの条件 2026-10-28 09:59:09
- Oracle 19c INSERT文|INSERT ALL・SELECT・RETURNINGとMERGEの使い分け 2026-10-28 09:59:09
- Oracle 19cのトランザクション制御|COMMIT・ROLLBACK・分離レベルと接続設定 2026-10-28 09:59:09