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.tsx と Suspense です。
実際に使ってみると、ローディング状態の管理がこんなにシンプルになるとは驚きでした。コード量は半分以下になり、ユーザー体験も一段上がります。本記事では、この仕組みの実践ノウハウを共有します。
なぜ 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>
);
}
一見問題なさそうですが、課題は次のとおりです。
- コードの重複:ページごとに同じ状態管理コードを書く必要がある
- 状態の分散:loading、data、error が別々の state に分かれ、同期ズレのバグが起きやすい
- Client Component 限定:useState と useEffect を使うため、コンポーネント全体がクライアント側で動き、SSR のメリットを失う
- 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>
);
}
デザインシステムとスタイルが揃うので、追加調整がほぼ不要です。
スケルトンスクリーンの設計原則
どのパターンでも、次の原則を守りましょう。
-
実レイアウトに合わせる:スケルトンの構造は本番コンテンツと一致させる。タイトル・概要・タグがあるなら、骨架も 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 境界を手動切り替える方法があります。
- React DevTools ブラウザ拡張をインストール
- 開発者ツールの Components タブを開く
<Suspense>コンポーネントを探す- 右クリック → 「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)を設けるか、スピナーではなくスケルトンスクリーンを使う方法もあります。
まとめ
核心ポイントをおさらいします。
-
loading.tsx はルート単位ローディングのベストプラクティス:ルートフォルダに置くだけ。Next.js が全部面倒を見てくれる。手書き useState から卒業し、コードが半分に。
-
スケルトンスクリーンはスピナーより UX が良い:レイアウトを先に見せ、不安を減らす。純 CSS、react-loading-skeleton、UI ライブラリ——プロジェクトに合わせて選ぶ。
-
Suspense はコンポーネントツリーの上位に:水門として下位の非同期を監視。位置を間違えると効かない。
-
動的ルートには key を忘れずに:ID 切り替えで loading が出ない問題は
<Suspense key={params.id}>で解決。 -
複数データソースは 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: 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: スケルトンスクリーンを実装
よりプロフェッショナルなローディング 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: Suspense で非同期コンポーネントをラップ
コンポーネント内で Suspense を使用:
• 非同期データ取得コンポーネントをラップ
• fallback でローディング状態を表示
• ネストした Suspense で細かい粒度の制御も可能
例:
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
複数コンポーネントはそれぞれ Suspense でラップ可能:
• 各コンポーネントが独立して読み込み
• 速いものから表示、遅いものは後から
• ユーザー体験が向上 - 4
ステップ4: 動的ルートの loading を処理
動的ルートの loading:
• 動的ルートディレクトリに loading.tsx を作成
• Next.js がパラメータ変更時のローディングを自動処理
• 手動で loading 状態を管理する必要なし
例:
app/products/[id]/
├── loading.tsx # パラメータ変更時に自動表示
└── page.tsx
/products/1 から /products/2 へ遷移すると、
loading.tsx が自動表示 - 5
ステップ5: ローディング体験を最適化
最適化のコツ:
• シンプルな Spinner よりスケルトンスクリーン
• ローディング UI を実コンテンツのレイアウトに合わせる
• animate-pulse などのアニメーションで体験向上
• Suspense を適切に使いストリーミングレンダリングを活用
避けること:
• あらゆる場所に loading.tsx を使う
• 複雑すぎるローディング UI
• エラー処理の neglect(error.tsx とセットで) - 6
ステップ6: テストと検証
テストのポイント:
• ページ遷移時のローディング状態
• 動的ルートのパラメータ変更時のローディング
• 低速ネットワークでの体験
• ローディング UI の滑らかさ
チェックリスト:
• すべてのルートに適切な loading 状態がある
• ローディング UI が実コンテンツのレイアウトと一致
• ちらつきやレイアウトシフトがない
• ユーザー体験がスムーズ
FAQ
loading.tsx と手書き useState の違いは?
loading.tsx はいつ表示される?
Suspense と loading.tsx の違いは?
スケルトンスクリーンはどう実装する?
動的ルートの loading はどう処理する?
loading のスタイルはカスタマイズできる?
loading.tsx はパフォーマンスに悪影響する?
6分で読めます · 公開日: 2026年1月5日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js 404 & 500 エラーページ完全カスタマイズガイド:技術実装からデザイン最適化まで
not-found.tsx、error.tsx、global-error.tsx の完全なコード例を含め、Next.js のエラーページをカスタマイズする方法を解説。ユーザー体験を向上させ、離脱率を下げるためのデザインのベストプラクティスと、よくある「404なのにステータス200」問題の解決策。
第 29 / 47 記事
次の記事
Next.js Error Boundary 完全ガイド:ランタイムエラーを上手に処理する 5 つのポイント
error.tsx の使い方、グローバルエラー処理、Server Components の例外の分け方、復旧の仕組みまで。Next.js の Error Boundary で白画面を防ぎ、ユーザー体験を守る実装を解説します。
第 31 / 47 記事
関連記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js Middleware 実践ガイド:パスマッチ、Edge Runtime 制限とよくある落とし穴
コメント
GitHubアカウントでログインしてコメントできます