6.

Next.js のストリーミング|loading.js と Suspense

編集

この記事の要点

  • ストリーミングとは、ページ全体の完成を待たずにできたところから順に送る仕組み。遅いデータが 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 側へ移します。

次は、そもそも待たなくて済むようにする側の話、つまりキャッシュに進みます。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 開発環境の構築とプロジェクトの初期構成
  2. App Router のルーティングとファイル規約
  3. レイアウトと特殊ファイルの使い分け
  4. サーバーコンポーネントとクライアントコンポーネント
  5. サーバー側でのデータ取得と並列化
  6. ストリーミングで体感速度を上げる
  7. キャッシュの仕組みと use cache
  8. 再検証の使い分け
  9. フォーム送信とデータ更新の実装
  10. エラーハンドリングとエラー境界
  11. Route Handlers で API を作る
  12. メタデータ API で SEO を整える
  13. 画像とフォントの最適化
  14. 本番ビルドとセルフホスト
  15. バージョン16 への移行と注意点