言語を切り替える
テーマを切り替える

Next.js ローディング状態管理完全ガイド:loading.tsx と Suspense で作る最高のユーザー体験

最近、あるレストランの予約サイトを使っていて気づいたことがあります。

ページを開いた瞬間、真っ白な画面が2秒間続き、突然すべてのコンテンツが「ドン!」と表示されるサイトと、
最初は枠組み(スケルトン)が表示され、徐々に画像やテキストが埋まっていくサイト。

前者を使うと「あれ? スマホの電波悪いかな?」と不安になりますが、後者だと「あ、今読み込んでるな」と安心して待てます。
体感速度は同じ2秒でも、心理的なストレスは天と地ほどの差があります。

これが「ローディング UI」の力です。

Next.js App Router は、この「待機時間」の体験を劇的に改善する仕組みを持っています。loading.tsx ファイルを置くだけで、React Suspense を使ったインスタントなローディング状態を作れるのです。

でも、ただ loading.tsx を置けばいいというわけではありません。
「どこに置くか」で、ユーザー体験は大きく変わります。ルート全体をブロックすべきか? それとも特定のコンポーネントだけを遅延させるか? スケルトンスクリーンはどうやって作るのが正解か?

この記事では、Next.js (App Router) におけるローディング状態管理のすべてを解説します。白画面(White Screen of Death)を撲滅し、ユーザーに「速い!」と思わせる魔法をかけましょう。

loading.tsx の魔法:仕組みを理解する

まずは基本から。App Router では、ファイルシステムベースのルーティングを採用していますが、ローディング状態もファイルシステムで定義します。

loading.tsx というファイルを作成すると、Next.js は自動的にその階層の page.tsxlayout.tsx<Suspense> でラップしてくれます。

実際に何が起きているのか?

以下のファイル構造を例にします:

app/
├── dashboard/
│   ├── layout.tsx
│   ├── loading.tsx  <-- これを作成
│   └── page.tsx

Next.js はビルド時に、これを以下のような React ツリーに変換します:

<Layout>
  <Suspense fallback={<Loading />}>
    <Page />
  </Suspense>
</Layout>

つまり:

  1. ユーザーが /dashboard にアクセスする。
  2. Layout は即座にレンダリングされる(サイドバーなどはすぐ見える)。
  3. Page がサーバーサイドでデータ取得をしている間、代わりに Loading コンポーネントが表示される。
  4. データ取得が完了すると、Loading が消えて Page が表示される。

これが「ストリーミングレンダリング」の正体です。サーバーは HTML を少しずつブラウザに送信できるので、ユーザーはずっと白い画面を見続ける必要がなくなります。

実践:シンプルなローディング画面

まずは簡単なスピナーを表示してみましょう。

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center min-h-[50vh]">
      <div className="w-10 h-10 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin"></div>
    </div>
  )
}

これだけで、ダッシュボードのデータ読み込み中にスピナーが表示されるようになります。簡単ですね。

スケルトンスクリーンで体験を向上させる

スピナーも悪くありませんが、もっと良い方法があります。「スケルトンスクリーン」です。
ページのレイアウトを模したグレーのボックスを表示することで、ユーザーに「コンテンツがどんな形で表示されるか」を予告します。これにより、体感速度が向上します(Perceived Performance)。

YouTube や Facebook がやっているアレです。

スケルトンの作り方

Tailwind CSS を使えば、animate-pulse クラスで簡単に作れます。

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="max-w-4xl mx-auto p-6 space-y-8">
      {/* タイトルのスケルトン */}
      <div className="h-10 bg-gray-200 rounded w-3/4 animate-pulse" />
      
      {/* メタ情報のスケルトン */}
      <div className="flex gap-4">
        <div className="h-4 bg-gray-200 rounded w-20 animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-20 animate-pulse" />
      </div>

      {/* 本文のスケルトン */}
      <div className="space-y-4">
        <div className="h-4 bg-gray-200 rounded w-full animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-full animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-5/6 animate-pulse" />
        <div className="h-4 bg-gray-200 rounded w-4/6 animate-pulse" />
      </div>
    </div>
  )
}

コツは、実際の page.tsx のレイアウトとできるだけ一致させることです。そうすれば、ローディングが終わった瞬間の「ガタつき(Layout Shift)」を防げます。

Suspense で部分的なローディングを実現する

loading.tsx はページ全体に対するローディングですが、時には「ページの一部だけ」をローディング中にしたいこともあります。

例えば、記事の本文はすぐ表示できるけど、コメント欄の読み込みだけ遅い場合。コメント欄のためにページ全体をブロックするのはもったいないですよね。

そんな時は、コンポーネント内で直接 <Suspense> を使います。

// app/post/[slug]/page.tsx
import { Suspense } from 'react'
import PostContent from '@/components/PostContent'
import PostComments from '@/components/PostComments'
import CommentsSkeleton from '@/components/CommentsSkeleton'

export default function PostPage({ params }) {
  return (
    <main>
      {/* 記事本文:ここはすぐ重要なデータなのでページ全体と一緒に待つか、高速なAPIなら即表示 */}
      <PostContent slug={params.slug} />

      <hr className="my-8" />

      {/* コメント欄:遅いので個別にLoading表示 */}
      <Suspense fallback={<CommentsSkeleton />}>
        <PostComments slug={params.slug} />
      </Suspense>
    </main>
  )
}

PostComments コンポーネント内で非同期データ取得(await fetch(...))を行っていれば、React は自動的にその完了を待ち、それまでは fallback を表示します。これが Streaming Server Components の真骨頂です。

ルート変更時のローディング:ナビゲーションの即時性

ここが少し混乱しやすいポイントです。
Next.js のリンク(<Link>)をクリックした時、何が起きるでしょうか?

  1. loading.tsx がある場合
    URL は即座に切り替わり、即座に loading.tsx の内容が表示されます。これを「即時ローディング状態(Instant Loading State)」と呼びます。ユーザーにとっては「サクサク動く」感覚になります。

  2. loading.tsx がない場合
    次のページのデータ取得が完了するまで、ブラウザは現在のページに留まります。URL は変わりません。読み込みが終わった瞬間にパッと次のページに切り替わります。

どちらが良いかはケースバイケースです。

  • ダッシュボードのようなアプリ:即座に反応してほしいので、loading.tsx(スケルトン)推奨。
  • ドキュメントサイト:読みかけの記事から勝手に飛ばされるより、準備ができてから遷移したほうが良い場合もある。その場合は loading.tsx を作らない、という選択肢もあります。

useTransition による「待機中」の表現

loading.tsx を作らず、現在のページに留まったまま「読み込み中…」と表示したい場合はどうすればいいでしょうか?
例えば、検索フィルターを適用した時、画面を白くせずにリストだけ更新したい場合などです。

useTransition フックを使います。

'use client'

import { useTransition } from 'react'
import { useRouter } from 'next/navigation'

export default function FilterButton() {
  const [isPending, startTransition] = useTransition()
  const router = useRouter()

  const handleFilter = () => {
    startTransition(() => {
      // ナビゲーションをトランジションとしてラップする
      router.push('/dashboard?filter=active')
    })
  }

  return (
    <button
      onClick={handleFilter}
      disabled={isPending}
      className={`px-4 py-2 rounded ${
        isPending ? 'bg-gray-400' : 'bg-blue-600'
      } text-white`}
    >
      {isPending ? '更新中...' : 'フィルター適用'}
    </button>
  )
}

こうすると、URL遷移(データ取得)が行われている間、isPendingtrue になります。画面は現在のまま維持され、ボタンだけがローディング状態になります。これを「ソフトナビゲーション」と呼びます。

クライアントの実践例:React DevTools でデバッグ

開発中、ローカル環境は速すぎてローディング画面が一瞬しか見えない(あるいは全く見えない)ことがあります。これではスケルトンのデザイン調整ができません。

3つの解決策があります。

  1. React DevTools を使う:
    コンポーネントツリーから <Suspense> を見つけ、右側の時計アイコン(“Suspend the selected component”)をクリックすると、強制的にフォールバック状態を表示できます。

  2. 人工的な遅延を入れる:
    データ取得関数で意図的に sleep させます。

    await new Promise(resolve => setTimeout(resolve, 3000)) // 3秒待つ

    ※ 本番コードから消すのを絶対に忘れないでください!

  3. Chrome のネットワークスロットリング:
    DevTools の Network タブで “Slow 3G” を選ぶ。ただし、これだとアセットの読み込みも遅くなるので少しストレスです。

まとめ:最高のローディング体験のために

ローディング状態は「待ち時間」ではなく「インターフェースの一部」です。

  1. 基本は loading.tsx:ルートごとのスケルトンを作って、即時反応するアプリ体験を作る。
  2. 重い部分は Suspense:ページ全体を待たせず、重いコンポーネントだけを粒度細かくストリーミングする。
  3. 継続的な操作は useTransition:フィルター変更やタブ切り替えなど、コンテキストを維持したい場合は画面遷移させずに状態だけ更新する。

ユーザーは「待つこと」が嫌いなのではなく、「何が起きているか分からないこと」が嫌いなのです。優れたローディング UI は、その不安を解消し、信頼感に変えることができます。

さあ、あなたのアプリから「真っ白な画面」を追放しましょう。

Next.js でスケルトンローディング画面を作成する

loading.tsx と Tailwind CSS を使用して、モダンなスケルトンローディング UI を実装する手順

  1. 1

    Step1: loading.tsx の作成

    ローディングを表示したいルートディレクトリ(例:app/dashboard/)に loading.tsx ファイルを作成します。
  2. 2

    Step2: スケルトンコンポーネントの作成

    実際のページレイアウトと似た構造のdiv要素を作成し、Tailwind CSS の `animate-pulse`、`bg-gray-200`、`rounded` クラスを適用して、脈動するグレーのボックスを作ります。
  3. 3

    Step3: レイアウトシフトの防止

    読み込み完了後のコンテンツと同じ高さ(height)や幅(width)をスケルトン要素に指定し、表示切り替え時の画面のガタつき(CLS)を防ぎます。
  4. 4

    Step4: 動作確認

    データ取得部分に一時的に `await new Promise(r => setTimeout(r, 2000))` を追加して遅延させ、スケルトンが正しく表示されるか確認します。

FAQ

loading.tsx と React Suspense の違いは何ですか?
loading.tsx は、Next.js が提供するファイル規約で、内部的にはページコンポーネントを自動的に <Suspense fallback={<Loading />}> でラップする仕組みです。手動で Suspense を書く手間を省き、ルート単位でのローディング画面を簡単に設定できます。
ページ遷移時にローディング画面を表示したくない場合は?
loading.tsx ファイルを作成しなければ、Next.js は次のページのデータ取得が完了するまで現在のページを表示し続けます(ブラウザのネイティブな遷移挙動に近くなります)。または、useTransition フックを使って、URL遷移をバックグラウンドで行い、UIの更新だけを制御することも可能です。
特定のコンポーネントだけをローディング中にするには?
ページ全体(loading.tsx)ではなく、そのコンポーネントを使う場所で直接 <Suspense fallback={<Skeleton />}> で囲みます。非同期コンポーネント(Async Server Component)であれば、データ取得が完了するまでフォールバックが表示され、他の部分は先にレンダリングされます。
loading.tsx が開発環境で一瞬しか表示されず確認できません。
データ取得ロジックに一時的な遅延(setTimeout)を入れるか、React Developer Tools の Suspense トグル機能を使うか、ブラウザのネットワークスロットリング機能を使って通信速度を落とすことで確認できます。

4 min read · 公開日: 2026年1月5日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事