この記事の要点
- ストリーミングとは、ページ全体の完成を待たずにできたところから順に送る仕組み。遅いデータが 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 / Maya)
- 7 Spring Frameworkのアノテーション一覧
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- プロジェクトをTomcatプロジェクトとして認識させる方法 2026-10-07 22:32:50
- MySQLの1366 Incorrect string value|Laravelの文字コード・絵文字エラー 2026-10-07 21:54:03
- curlの証明書ホスト名不一致|旧エラー51・現行60の確認と対処 2026-10-07 21:54:03
- LaravelのMassAssignmentException|fillableの原因と安全な対処 2026-10-07 21:54:03
- Eclipse で Tomcat の起動ログがコンソールに出ない時の確認手順 2026-10-07 21:54:02
- MySQLにおける中央値(Median)の導き方(バージョン8未満) 2026-10-07 13:49:45
- getInputForward 2026-10-07 13:41:15
- JSONから配列に変換 2026-10-07 13:41:15
- ビューから値をモデルに格納しコントローラーで受け取る方法 2026-10-07 13:23:41
- Laravelのテーブル作成と定義変更|マイグレーション・up/down・注意点 2026-10-07 13:23:41
- NumPy 配列に要素を追加する方法 (append / concatenate) 2026-10-07 13:23:41
- MariaDB・MySQLで現在日時を取得する方法|NOW・タイムゾーン・保存型 2026-10-07 13:13:36
- 【django】テンプレートで定数を使用する方法 2026-10-07 13:10:15
- Spring BootにおけるApplication.propertiesの環境依存設定の分割方法 2026-10-07 12:09:35
- Not supported for DML operations【Springエラー】 2026-10-07 11:09:38