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

Next.js ローディング状態管理:loading.tsx と Suspense 実践ガイド

こんな経験はありませんか。リンクをクリックしたのに、画面が 3 秒ほど真っ白のまま何の反応もない。ユーザーは「フリーズした?」と不安になり、F5 を連打する——せっかく読み込まれたページまで消えてしまう……。

以前の私も、ローディングはまさにこう処理していました。新しいページを作るたびに、コンポーネントに次のようなコードを足していました。

const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);

useEffect(() => {
  setLoading(true);
  fetchData()
    .then(setData)
    .finally(() => setLoading(false));
}, []);

if (loading) return <Spinner />;

コードは冗長で、ページごとに同じことを繰り返す必要があります。さらに厄介なのは、チームメンバーごとにローディングの書き方がバラバラなこと。グローバル状態を使う人、Context を使う人、コンポーネント内で各自実装する人——プロジェクトが大きくなるほど、メンテナンスは悪夢です。

ある日、Next.js の公式ドキュメントを読んで気づきました。Next.js には、もっとエレガントなローディング管理の仕組みが最初から組み込まれている——loading.tsxSuspense です。

実際に使ってみると、ローディング状態の管理がこんなにシンプルになるとは驚きでした。コード量は半分以下になり、ユーザー体験も一段上がります。本記事では、この仕組みの実践ノウハウを共有します。

なぜ loading.tsx と Suspense を使うのか

従来アプローチの課題

まず、具体的な例を見てみましょう。ブログ一覧ページを作る場合、従来の書き方はだいたいこうなります。

// app/blog/page.tsx
'use client';
import { useState, useEffect } from 'react';

export default function BlogPage() {
  const [loading, setLoading] = useState(true);
  const [posts, setPosts] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => {
        setPosts(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <div className="spinner">Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

一見問題なさそうですが、課題は次のとおりです。

  1. コードの重複:ページごとに同じ状態管理コードを書く必要がある
  2. 状態の分散:loading、data、error が別々の state に分かれ、同期ズレのバグが起きやすい
  3. Client Component 限定:useState と useEffect を使うため、コンポーネント全体がクライアント側で動き、SSR のメリットを失う
  4. UX が悪い:クリックからローディング表示まで、白画面のカクつきが目立つ

Code Review を経験した人なら分かると思いますが、開発者ごとにローディング処理は千差万別。Context に上げる人、Zustand でグローバル管理する人、各コンポーネントでバラバラに書く人——プロジェクトが大きくなると、保守はほぼ破綻します。

Next.js の解決策

Next.js の App Router には、これらの問題に対応する 3 つの核心機能があります。

1. loading.tsx — 設定より規約

ルートフォルダに loading.tsx を置くだけで、Next.js がそのルートのローディング UI として自動的に使います。useState を手書きする必要も、状態管理も、Suspense のラップも不要です。

2. Suspense — React 18 ネイティブ対応

React 18 の Suspense なら、コンポーネント単位でローディングを細かく制御できます。遅いデータ部分だけ Suspense 境界を張れば、他の部分は先に表示できます。ページ全体を待たせる必要はありません。

3. Streaming — 読み込みながら表示

Next.js のストリーミングレンダリングと組み合わせると、ページを部分的に順次表示できます。ヘッダーが先、サイドバーが次、遅いデータ部分が最後——白画面で待たせず、体験が大きく改善します。

参考までに、スケルトンスクリーンと Streaming を使うと、FCP(First Contentful Paint)や LCP(Largest Contentful Paint)を短縮でき、Google PageSpeed Insights のスコアも数ポイント上がることがあります。

loading.tsx の基本

クイックスタート:最初の loading.tsx

さっそく、最もシンプルな loading.tsx を書いてみましょう。

ディレクトリ構造は次のような想定です。

app/
  blog/
    page.tsx

blog フォルダに loading.tsx を追加するだけです。

app/
  blog/
    loading.tsx  ← 追加
    page.tsx

loading.tsx にはシンプルなローディング UI を書きます。

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
      <p className="ml-4">読み込み中...</p>
    </div>
  );
}

これだけです。10 行程度で完了。ユーザーが /blog にアクセスすると、page.tsx の読み込みが終わるまで、Next.js がこのローディングコンポーネントを自動表示します。

ポイント:Suspense を手動でラップする必要はありません。Next.js が page.tsx<Suspense fallback={<Loading />}> で包んでくれます。

初めて見たときは「こんなに簡単で本当に動くの?」と半信半疑でした。試してみると、ちゃんと動きます。各ページの useState 地獄から解放され、コードもかなりすっきりしました。

loading.tsx の作用範囲

loading.tsx には**ルートセグメント(Route Segment)**という重要な概念があります。同じフォルダ内の page.tsx と、その配下のすべての子ルートに作用します。

例:

app/
  blog/
    loading.tsx     ← /blog と /blog/[id] の両方に作用
    page.tsx        ← /blog 一覧
    [id]/
      page.tsx      ← /blog/123 詳細

この loading.tsx は次のタイミングで表示されます。

  • ユーザーが /blog(一覧)にアクセスしたとき
  • 一覧から /blog/123(詳細)へ遷移したとき

ただし、layout には影響しませんblog/layout.tsx にナビゲーションがある場合、ナビは常に表示されたまま、page.tsx の部分だけがローディング UI に置き換わります。

これが Next.js 公式ドキュメントの「共有レイアウトはインタラクティブのまま」という意味です。新しいページの読み込みを待っている間も、ナビから別ページへ切り替えられ、画面全体が固まることはありません。

構造を図にすると次のようになります。

Layout(常に表示)
  ├─ ナビゲーション
  └─ Suspense Boundary
       ├─ Loading UI(データ読み込み中)
       └─ Page(読み込み完了後)

Server Component と Client Component

loading.tsx はデフォルトで Server Component です。多くの場合、JSX を返すだけで十分です。

アニメーションを Framer Motion で付けたい、クライアント JavaScript が必要なライブラリを使いたい——そんなときは 'use client' を付けます。

// app/blog/loading.tsx
'use client';
import { motion } from 'framer-motion';

export default function Loading() {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      className="flex items-center justify-center min-h-screen"
    >
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
    </motion.div>
  );
}

私の原則は、クライアント側のインタラクションが本当に必要なとき以外は Server Component を使うこと。Server Component はクライアント bundle に含まれないので、ページ読み込みが速くなります。

スケルトンスクリーン実践

スピナーよりスケルトンスクリーンが優れる理由

くるくる回るローディングスピナー、よく見かけますよね。UX の観点では、スケルトンスクリーン(Skeleton Screen)のほうがはるかに優れています

理由は、ユーザー心理の研究でも示されています。スケルトンスクリーンを見ると、脳は「もうすぐ内容が出てくる」と予測し、待ち時間の体感が短くなります。スピナーだと「読み込み中」は分かっても、何が来るのか・どれくらい待つのかが分からず、不安が強まります。

スケルトンスクリーンは、ページのおおよそのレイアウトも先に伝えられます。横長のブロックが 3 つ並んでいれば、記事が 3 本あると分かる。期待が持てるので、慌てにくくなります。

3 つの実装パターン

実装方法はいろいろありますが、よく使う 3 つを紹介します。プロジェクトに合わせて選んでください。

パターン 1:純 CSS(最軽量)

追加依存なしで済ませたいなら、純 CSS で十分です。

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="max-w-4xl mx-auto p-6">
      {[1, 2, 3].map((i) => (
        <div key={i} className="mb-8 animate-pulse">
          {/* タイトル骨架 */}
          <div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
          {/* 概要骨架 */}
          <div className="space-y-2">
            <div className="h-4 bg-gray-200 rounded"></div>
            <div className="h-4 bg-gray-200 rounded w-5/6"></div>
          </div>
          {/* メタ情報骨架 */}
          <div className="flex gap-4 mt-4">
            <div className="h-3 bg-gray-200 rounded w-20"></div>
            <div className="h-3 bg-gray-200 rounded w-24"></div>
          </div>
        </div>
      ))}
    </div>
  );
}

メリットはゼロ依存でパフォーマンスが良いこと。デメリットはスタイルを自分で書く手間です。

パターン 2:react-loading-skeleton(最速)

スタイルをあまり書きたくないなら react-loading-skeleton が便利です。

npm install react-loading-skeleton
// app/blog/loading.tsx
'use client';
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';

export default function Loading() {
  return (
    <div className="max-w-4xl mx-auto p-6">
      {[1, 2, 3].map((i) => (
        <div key={i} className="mb-8">
          <Skeleton height={32} width="75%" className="mb-4" />
          <Skeleton count={2} />
          <div className="flex gap-4 mt-4">
            <Skeleton width={80} />
            <Skeleton width={100} />
          </div>
        </div>
      ))}
    </div>
  );
}

使いやすく、アニメーションもきれい。小規模プロジェクトではよく使っています。

パターン 3:shadcn/ui(最も統一感)

すでに shadcn/ui を使っているなら、Skeleton コンポーネントが一番楽です。

npx shadcn-ui@latest add skeleton
// app/blog/loading.tsx
import { Skeleton } from '@/components/ui/skeleton';

export default function Loading() {
  return (
    <div className="max-w-4xl mx-auto p-6">
      {[1, 2, 3].map((i) => (
        <div key={i} className="mb-8">
          <Skeleton className="h-8 w-3/4 mb-4" />
          <Skeleton className="h-4 w-full mb-2" />
          <Skeleton className="h-4 w-5/6 mb-4" />
          <div className="flex gap-4">
            <Skeleton className="h-3 w-20" />
            <Skeleton className="h-3 w-24" />
          </div>
        </div>
      ))}
    </div>
  );
}

デザインシステムとスタイルが揃うので、追加調整がほぼ不要です。

スケルトンスクリーンの設計原則

どのパターンでも、次の原則を守りましょう。

  1. 実レイアウトに合わせる:スケルトンの構造は本番コンテンツと一致させる。タイトル・概要・タグがあるなら、骨架も 3 ブロックに分ける。

  2. 控えめなアニメーション:点滅はさりげなく。派手すぎると注意が散り、待ち時間が長く感じる。

  3. 適切な件数:3〜5 件程度で十分。画面いっぱい並べる必要はない。

実例:ブログ一覧ページの完成形

ここまでの内容をまとめ、ブログ一覧の完成例を見てみましょう。

まず loading.tsx

// app/blog/loading.tsx
export default function BlogLoading() {
  return (
    <div className="max-w-4xl mx-auto px-4 py-8">
      <div className="h-12 bg-gray-200 rounded w-1/3 mb-8 animate-pulse"></div>

      <div className="space-y-8">
        {[1, 2, 3].map((i) => (
          <article key={i} className="border-b pb-8 animate-pulse">
            <div className="h-8 bg-gray-200 rounded w-3/4 mb-3"></div>
            <div className="space-y-2 mb-4">
              <div className="h-4 bg-gray-200 rounded"></div>
              <div className="h-4 bg-gray-200 rounded w-11/12"></div>
              <div className="h-4 bg-gray-200 rounded w-4/5"></div>
            </div>
            <div className="flex gap-3">
              <div className="h-6 bg-gray-200 rounded-full w-16"></div>
              <div className="h-6 bg-gray-200 rounded-full w-20"></div>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

次に Server Component の page.tsx

// app/blog/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store' // 毎回再取得
  });

  if (!res.ok) throw new Error('Failed to fetch posts');

  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <div className="max-w-4xl mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8">ブログ記事</h1>

      <div className="space-y-8">
        {posts.map((post) => (
          <article key={post.id} className="border-b pb-8">
            <h2 className="text-2xl font-semibold mb-3">
              <a href={`/blog/${post.slug}`} className="hover:text-blue-600">
                {post.title}
              </a>
            </h2>
            <p className="text-gray-600 mb-4">{post.excerpt}</p>
            <div className="flex gap-3">
              {post.tags.map((tag) => (
                <span key={tag} className="px-3 py-1 bg-gray-100 rounded-full text-sm">
                  {tag}
                </span>
              ))}
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

page.tsx が async 関数になり、コンポーネント内で直接 await できます。useState も useEffect も不要。コードがすっきりします。

Server Component なので処理はサーバー側で完結し、クライアント bundle も増えません。初回表示が速くなります。

デバッグのコツ:React DevTools でテスト

開発中、データが速すぎてローディング UI が一瞬で消え、スタイル調整が難しい——そんなことありませんか。

React DevTools で Suspense 境界を手動切り替える方法があります。

  1. React DevTools ブラウザ拡張をインストール
  2. 開発者ツールの Components タブを開く
  3. <Suspense> コンポーネントを探す
  4. 右クリック → 「Suspend this Suspense boundary」を選択

ローディング UI が固定表示され、ゆっくりスタイルを調整できます。終わったら suspend を解除するだけ。

正直、何度かハマってからこの機能に気づきました。早く知っていれば、相当時間を節約できたはずです。

Suspense 応用テクニック

手動で Suspense 境界を設定する

loading.tsx は便利ですが、より細かい制御が必要な場面もあります。ページに複数の独立したデータソースがあり、全部そろうまで待たず、それぞれ個別にローディングを出したい——そんなときは手動で Suspense 境界を張ります。

よくある間違い:Suspense をデータ取得コンポーネントの内部に置くこと。

// ❌ 誤った例 — Suspense が低すぎる
async function PostList() {
  const posts = await fetchPosts();

  return (
    <Suspense fallback={<Loading />}>  {/* これでは動かない! */}
      <div>
        {posts.map(post => <Post key={post.id} {...post} />)}
      </div>
    </Suspense>
  );
}

これでは効きません。Suspense はコンポーネントツリーの上位に置き、下位の非同期処理を「捕まえる」必要があるからです。

正しくは親コンポーネントに Suspense を置きます。

// ✅ 正しい例 — 親コンポーネントに Suspense
export default function BlogPage() {
  return (
    <div>
      <h1>ブログ記事</h1>

      <Suspense fallback={<PostListSkeleton />}>
        <PostList />
      </Suspense>
    </div>
  );
}

// 子コンポーネントでデータ取得
async function PostList() {
  const posts = await fetchPosts();

  return (
    <div>
      {posts.map(post => <Post key={post.id} {...post} />)}
    </div>
  );
}

Suspense は水門のようなもの。ツリー上のある位置に立ち、下位の非同期処理を監視します。下に待ち状態のコンポーネントがあれば水門を閉じ、fallback を表示。データが揃えば開いて本番 UI を出します。

動的ルートの特殊対応

ここは私も深くハマったポイントなので、重点的に説明します。

商品詳細 /products/[id] で、商品 A(id=1)から商品 B(id=2)へ切り替えると——loading.tsx が表示されないことがあります。

内容が商品 A から B にパッと切り替わり、ローディングの遷移がなく、体験が唐突に感じられます。

React には最適化があり、コンポーネントの型が同じ(どちらも ProductPage)ならインスタンスを再利用し、props だけ更新します。Suspense から見ると「コンポーネントは変わっていない」と判断され、再 suspend されないのです。

解決策:Suspense に key 属性を付け、React に「新しいコンポーネントとして再レンダリングして」と伝えます。

// app/products/[id]/page.tsx
import { Suspense } from 'react';

export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <Suspense key={params.id} fallback={<ProductSkeleton />}>
      <ProductDetail id={params.id} />
    </Suspense>
  );
}

async function ProductDetail({ id }: { id: string }) {
  const product = await fetchProduct(id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span>${product.price}</span>
    </div>
  );
}

注目は <Suspense key={params.id} ...> の一行。

id が変わると React は古い Suspense インスタンスを破棄し、新しく作り直します。新インスタンスは再び suspend 状態に入り、ローディング UI が正常に表示されます。

この問題で半日ハマり、GitHub の issue で key のテクニックを見つけて試したら、すぐ直りました。知っていれば一瞬、知らないと地獄——そんなパターンです。

複数ローディング状態の調整

最後に、やや複雑なケース。ページで複数のデータソースを同時に読み込む場合。

ダッシュボードでユーザー情報・統計・最近のアクティビティの 3 つを API で取るとします。戦略は 2 つ。

戦略 1:全部そろってから表示(1 つの Suspense で全部ラップ)

export default function Dashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <UserInfo />      {/* API 1 */}
      <Statistics />    {/* API 2 */}
      <RecentActivity /> {/* API 3 */}
    </Suspense>
  );
}

メリット:実装がシンプル、一度に完成形を表示
デメリット:最遅 API に全体が引っ張られ、待ち時間 = 最遅 API の時間

戦略 2:段階表示(複数の Suspense 境界)

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<UserInfoSkeleton />}>
        <UserInfo />
      </Suspense>

      <Suspense fallback={<StatsSkeleton />}>
        <Statistics />
      </Suspense>

      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

メリット:速い部分から順に表示、体感待ち時間が短い
デメリット:レイアウトが「跳ねる」。読み込みに合わせて配置が変わり、目が疲れることも

私の選び方はデータの重要度次第です。

  • コアデータ(ユーザー情報など)は 1 つの Suspense でまとめて表示
  • 二次データ(おすすめ、広告など)は別 Suspense で非同期読み込み

コア体験を保ちつつ、全データを待たせないバランスが取れます。

よくある問題と解決策

Suspense が効かないとき

Suspense が動かない場合、次を確認してください。

1. データ取得方式

Suspense が効くのは「Suspense 対応の取得方式」だけです。Next.js App Router では:

  • ✅ Server Component 内で直接 await(推奨)
  • ✅ Suspense 対応ライブラリ(SWR、React Query など)
  • ❌ useEffect 内の fetch(非対応)
  • ❌ 従来の Promise.then(非対応)

2. コンポーネントの位置

Suspense はデータ取得コンポーネントの上位に置く。同じコンポーネント内や下位では効きません。

3. バージョン

次を満たしているか確認:

  • React 18+
  • Next.js 13+(App Router)

4. デバッグ

React DevTools で Suspense 境界を手動 suspend。手動でも反応がなければ Suspense 自体が効いていないので、上記を再確認。

useFormStatus フックの落とし穴

Server Actions でフォーム送信する場合、useFormStatus で送信状態を表示することがあります。

落とし穴:useFormStatus は Client Component 内でのみ動作します。

ただし form 自体は Server Component でレンダリングする必要があります。そうでないと Server Action を bind できません。

正しい構成:Server Component が form を描画し、Client Component が状態を表示

// app/actions.ts
'use server';
export async function submitForm(formData: FormData) {
  // フォーム処理...
  await saveToDatabase(formData);
}
// app/page.tsx (Server Component)
import { submitForm } from './actions';
import { SubmitButton } from './submit-button';

export default function Page() {
  return (
    <form action={submitForm}>
      <input name="email" type="email" />
      <SubmitButton />
    </form>
  );
}
// app/submit-button.tsx (Client Component)
'use client';
import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? '送信中...' : '送信'}
    </button>
  );
}

form は Server Component、button は Client Component。Server Action とローディング状態の両方が正常に動きます。

プリフェッチ(Prefetch)が loading に与える影響

Next.js の <Link> は、リンクがビューポートに入るとデフォルトでページをプリフェッチします。

その結果、クリック時に loading が一瞬で消えたり、表示されないこともあります。データが先に取れているからです。

ローディング UI をテストしたいときは、一時的にプリフェッチをオフにできます。

<Link href="/blog" prefetch={false}>
  ブログ
</Link>

本番ではプリフェッチ ON のままがおすすめ。loading の表示時間が短すぎる場合は、最小表示時間(例:300ms)を設けるか、スピナーではなくスケルトンスクリーンを使う方法もあります。

まとめ

核心ポイントをおさらいします。

  1. loading.tsx はルート単位ローディングのベストプラクティス:ルートフォルダに置くだけ。Next.js が全部面倒を見てくれる。手書き useState から卒業し、コードが半分に。

  2. スケルトンスクリーンはスピナーより UX が良い:レイアウトを先に見せ、不安を減らす。純 CSS、react-loading-skeleton、UI ライブラリ——プロジェクトに合わせて選ぶ。

  3. Suspense はコンポーネントツリーの上位に:水門として下位の非同期を監視。位置を間違えると効かない。

  4. 動的ルートには key を忘れずに:ID 切り替えで loading が出ない問題は <Suspense key={params.id}> で解決。

  5. 複数データソースは Suspense を分割:コアはまとめ、二次は非同期。体験とパフォーマンスのバランス。

手書き useState から loading.tsx へ——これは作業量の増加ではなく、もっと賢く働くことです。コードが減り、バグが減り、UX が上がる。やらない理由はないでしょう。

次のステップ

今すぐ試すなら、次をおすすめします。

今すぐ試す:既存プロジェクトでシンプルな一覧ページを 1 つ選び、loading を loading.tsx に置き換える。一度手を動かす方が、10 記事読むより効きます。

次の学習:loading ができたら Error Boundaries へ。loading が読み込み、Error Boundaries がエラーを担当する——セットで考えると理解が深まります。Error Boundaries の実践記事も後日書く予定なので、またお会いしましょう。

経験を共有:あなたのプロジェクトではローディングをどう処理していますか。どんな方案を使い、どんな落とし穴がありましたか。コメントでぜひ教えてください。


参考資料

Next.js ローディング状態管理の完全フロー

loading.tsx と Suspense でプロ級のローディング体験を実現。手書き useState から卒業

⏱️ 目安時間: 1 時間

  1. 1

    ステップ1: loading.tsx ファイルを作成

    ルートディレクトリに loading.tsx を作成:
    • app/dashboard/loading.tsx:dashboard ルートのローディング状態
    • app/products/[id]/loading.tsx:動的ルートのローディング状態

    ファイル内容:
    export default function Loading() {
    return <div>読み込み中...</div>
    }

    Next.js がページ読み込み時にこのコンポーネントを自動表示
  2. 2

    ステップ2: スケルトンスクリーンを実装

    よりプロフェッショナルなローディング UI を作成:
    • Skeleton コンポーネントでコンテンツレイアウトを模倣
    • 実際のコンテンツに近いレイアウトを維持
    • アニメーションで体験を向上

    例:
    export default function Loading() {
    return (
    <div className="animate-pulse">
    <div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
    <div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
    <div className="h-4 bg-gray-200 rounded w-5/6"></div>
    </div>
    )
    }
  3. 3

    ステップ3: Suspense で非同期コンポーネントをラップ

    コンポーネント内で Suspense を使用:
    • 非同期データ取得コンポーネントをラップ
    • fallback でローディング状態を表示
    • ネストした Suspense で細かい粒度の制御も可能

    例:
    <Suspense fallback={<Loading />}>
    <AsyncComponent />
    </Suspense>

    複数コンポーネントはそれぞれ Suspense でラップ可能:
    • 各コンポーネントが独立して読み込み
    • 速いものから表示、遅いものは後から
    • ユーザー体験が向上
  4. 4

    ステップ4: 動的ルートの loading を処理

    動的ルートの loading:
    • 動的ルートディレクトリに loading.tsx を作成
    • Next.js がパラメータ変更時のローディングを自動処理
    • 手動で loading 状態を管理する必要なし

    例:
    app/products/[id]/
    ├── loading.tsx # パラメータ変更時に自動表示
    └── page.tsx

    /products/1 から /products/2 へ遷移すると、
    loading.tsx が自動表示
  5. 5

    ステップ5: ローディング体験を最適化

    最適化のコツ:
    • シンプルな Spinner よりスケルトンスクリーン
    • ローディング UI を実コンテンツのレイアウトに合わせる
    • animate-pulse などのアニメーションで体験向上
    • Suspense を適切に使いストリーミングレンダリングを活用

    避けること:
    • あらゆる場所に loading.tsx を使う
    • 複雑すぎるローディング UI
    • エラー処理の neglect(error.tsx とセットで)
  6. 6

    ステップ6: テストと検証

    テストのポイント:
    • ページ遷移時のローディング状態
    • 動的ルートのパラメータ変更時のローディング
    • 低速ネットワークでの体験
    • ローディング UI の滑らかさ

    チェックリスト:
    • すべてのルートに適切な loading 状態がある
    • ローディング UI が実コンテンツのレイアウトと一致
    • ちらつきやレイアウトシフトがない
    • ユーザー体験がスムーズ

FAQ

loading.tsx と手書き useState の違いは?
loading.tsx は Next.js の規約で、ページ読み込み時に自動表示され、状態を手動管理する必要がありません。手書き useState はページごとに同じコードを書き、重複とミスが起きやすいです。loading.tsx ならコード量を約 50% 削減でき、UX も向上。ストリーミングレンダリングにも対応します。
loading.tsx はいつ表示される?
loading.tsx は次のタイミングで自動表示されます:1) ユーザーがそのルートへ遷移したとき;2) 動的ルートのパラメータが変わったとき;3) 親ルートの読み込み中。Next.js が表示タイミングを管理するため、手動制御は不要です。ページデータの読み込みが完了すると、loading.tsx は自動的に非表示になります。
Suspense と loading.tsx の違いは?
loading.tsx はルート単位のローディング状態で、ページ全体に適用されます。Suspense はコンポーネント単位で、特定の非同期コンポーネントをラップし、より細かい粒度の制御が可能です。両方併用できます:ルートは loading.tsx、コンポーネント内は Suspense。
スケルトンスクリーンはどう実装する?
loading.tsx 内で Skeleton コンポーネントを使い、実際のコンテンツレイアウトを模倣します。Tailwind の animate-pulse クラスでアニメーションを付けられます。骨架のレイアウトを本番コンテンツと揃えると、読み込み完了後のレイアウトシフトを防げます。
動的ルートの loading はどう処理する?
動的ルートディレクトリに loading.tsx を置くだけです。例:app/products/[id]/loading.tsx。ユーザーが /products/1 から /products/2 へ遷移すると、Next.js が loading.tsx を自動表示し、パラメータ変更の管理は不要です。
loading のスタイルはカスタマイズできる?
はい。loading.tsx は通常の React コンポーネントなので、スタイルを自由にカスタマイズできます。Tailwind CSS、CSS Modules、styled-components など、任意の方法が使えます。UX 向上のため、シンプルな Spinner よりスケルトンスクリーンを推奨します。
loading.tsx はパフォーマンスに悪影響する?
いいえ、むしろ改善します。loading.tsx はストリーミングレンダリング(Streaming)に対応し、ページを分割して読み込めます。ユーザーはページ全体の完了を待たず、速い部分から表示できます。全体の体験が向上します。

6分で読めます · 公開日: 2026年1月5日 · 更新日: 2026年6月8日

関連記事

コメント

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