0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsのloading.tsxと<Suspense>で段階的にローディング表示を改善してみた

Last updated at Posted at 2025-04-13

はじめに

この記事では、Next.js App Routerにおけるローディング体験を、

  • loading.tsx を使ったページ全体のローディング表示
  • <Suspense> を使った部分的・段階的なローディング表示

の2つの手法で比較しながら、それぞれの使いどころを学びます。
最終的に、以下のように段階的なローディング表示を行います。
step3.gif

デモアプリ作成(スキップ可)

ローディングを表示するためのアプリを作成します。
ローディング実装のみ見たい方は 飛ばしてください。
今回はchatGPTを使用してローディング以外の部分を作成してみます。
ダミーの記事一覧表示アプリを作成します。

プロジェクト作成

🛠️ Next.js プロジェクト作成手順(App Router + TypeScript + Tailwind)

✅ 1. プロジェクトの作成

npx create-next-app@latest loading-ux-demo \
  --typescript \
  --app \
  --tailwind \
  --eslint \
  --src-dir \
  --import-alias "@/*"

これで以下が自動で設定されます:

  • TypeScript対応
  • app/ディレクトリ(App Router構成)
  • Tailwind CSS
  • ESLint
  • src/ ディレクトリ構成
  • @/src/ をエイリアスとして使える

✅ 2. ディレクトリ構成に合わせてファイルを作成
以下のようにファイルを追加:

cd loading-ux-demo

# ページとコンポーネントのディレクトリを作成
mkdir -p src/app/articles
mkdir -p src/components

# ファイルを作成(あとで中身をコピペ)
touch src/app/articles/page.tsx
touch src/components/article-list.tsx
touch src/components/article-card.tsx

loading.tsx は Next.js が app/articles/loading.tsx を自動認識するので、後で追加してもOKです。

✅ 3. Tailwind のアニメーションを使う準備(特別な設定は不要)
Tailwind CSS は create-next-app の時点で導入されているので、
skeleton 用の animate-pulsebg-gray-200 はすぐに使えます。

✅ 4. 動作確認コマンド

npm run dev

そしてブラウザで http://localhost:3000/articles にアクセス!

ディレクトリ構成
app/
├── articles/
│   ├── page.tsx          ← 遅延付きの記事一覧ページ
├── components/
│   ├── article-list.tsx  ← 記事一覧コンテナ
│   ├── article-card.tsx  ← 遅延付きの 1 記事表示
以下、各コンポーネントの実装。ローディングを挟むために、ところどころ擬似的な重い処理を入れています。
ホーム画面の実装(app/articles/page.tsx)
import ArticleList from "@/components/article-list";

export default async function ArticlesPage() {
  //* 重いローディング処理 */
  await new Promise((res) => setTimeout(res, 1500));
  return (
    <main className="p-6">
      <h1 className="text-2xl font-bold mb-4">記事一覧</h1>
      <ArticleList />
    </main>
  );
}
記事一覧画面の実装1(app/articles/article-list.tsx)
import ArticleCard from "./article-card";

const articleIds = [1, 2, 3];

export default function ArticleList() {
  return (
    <div className="space-y-4">
      {articleIds.map((id) => (
        <ArticleCard key={id} id={id} />
      ))}
    </div>
  );
}
記事一覧画面の実装2(app/articles/article-card.tsx)
export type Props = {
  id: number;
};

async function fetchArticle(id: number) {
  //* 重いローディング処理 */
  await new Promise((res) => setTimeout(res, 1000 + Math.random() * 2000));
  return {
    title: `記事 ${id}`,
    author: ["佐藤", "田中", "鈴木"][id % 3],
  };
}

export default async function ArticleCard({ id }: Props) {
  const article = await fetchArticle(id);

  return (
    <div className="border p-4 rounded shadow">
      <h2 className="text-lg font-semibold">{article.title}</h2>
      <p className="text-sm text-gray-500">by {article.author}</p>
    </div>
  );
}

ローディングの実装

ローディングの実装をステップに分けて行います。
これにより、どのタイミングでどのローディングコンポーネントが表示されるのか分かりやすくなります。

Step 1(ローディングなしの初期状態)

まず、ローディング表示を何も実装していない初期状態です。

この状態では、すべての Promise が解決されてから一気にページ全体が描画されるため、読み込み中であることがユーザーに伝わりません

特に通信環境が遅い場合、「無反応な画面」のまましばらく待たされることになり、UXとしてはやや不親切です。

step1.gif

ホーム画面

image.png

記事一覧画面

image.png

Step 2(ローディング画面の一括表示)

次に、loading.tsx を配置した場合です。
Next.js は loading.tsx を自動的に認識し、同階層にある page.tsx の読み込みがサスペンドされたときに表示されます。

loading.tsx を用意することで、ページ読み込み中の「待ち時間」 を明示でき、簡単にローディングUIを挟むことができます。

具体的には、page.tsxasync 関数であり、内部で await による非同期処理(例:データフェッチ)を含む場合、その遅延に応じて loading.tsx が表示されます。

step2.gif

以下のファイルを新規作成

app/articles/loading.tsx
+ export default function Loading() {
+   return (
+     <div className="min-h-screen flex flex-col items-center justify-center text-center p-6">
+       <div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-400 border-t-transparent mb-6" />
+       <p className="text-gray-600 text-lg">記事一覧を読み込んでいます...</p>
+     </div>
+   );
+ }

💡ローディング表示の内訳
以下の処理を待つ間、loading.tsxが表示さています
page.tsx冒頭

//* 重いローディング処理 */
await new Promise((res) => setTimeout(res, 1500));

fetchArticle()冒頭

//* 重いローディング処理 */
await new Promise((res) => setTimeout(res, 1000 + Math.random() * 2000));

step3(段階的なローディング表示)

<Suspense> タグを使用することで、段階的なローディング表示が可能になります。

  • <ArticleCard><Suspense> でラップすることで、内部で await が発生した場合、指定した fallback(ここでは Skeleton)が一時的に表示されます
  • Skeleton が表示されると、page.tsx 内のすべての Promise が解決されたとみなされ、loading.tsx のローディング画面が解除されます
  • この時点で <ArticleCard> 自体はまだ読み込み中でも、代わりに Skeleton が表示されることで、ページ全体の遷移が早く見えるようになります
  • なお、<ArticleCard> はランダムなローディング時間を伴うため、読み込みが完了した記事から順に表示されるという、より自然でスムーズな体験が実現されます

step3.gif

差分(Step2 → Step3)
// components/article-list.tsx の変更

export default function ArticleList() {
  return (
    <div className="space-y-4">
-      {articleIds.map((id) => (
-        <ArticleCard key={id} id={id} />
-      ))}
+       {articleIds.map((id) => (
+         <Suspense key={id} fallback={<Skeleton />}>
+           <ArticleCard id={id} />
+         </Suspense>
+       ))}
   </div>
  );
}
+ function Skeleton() {
+  return <div className="animate-pulse bg-gray-200 rounded h-20 w-full" />;
+ }

💡ローディング表示の内訳
page.tsx冒頭の以下の処理を待つ間、loading.tsx が表示さています

//* 重いローディング処理 */
await new Promise((res) => setTimeout(res, 1500));

そして、fetchArticle冒頭の以下の処理を待つ間に、Skeltonが表示されています

//* 重いローディング処理 */
await new Promise((res) => setTimeout(res, 1000 + Math.random() * 2000));

💡 レイアウトシフトを防ぐための工夫

段階的に描画されることでUXは向上しますが、ローディング中のスケルトンと表示後のレイアウトが大きく異なると、「ガタつき(レイアウトシフト)」が起きてしまいUXを損なう可能性があります。

そのため、Skeleton コンポーネントは最終的な ArticleCard に近いサイズ・構成にするように工夫しています。

function Skeleton() {
  return (
    <div className="animate-pulse bg-gray-200 rounded h-20 w-full" />
  );
}

🛠 開発中にローディング表示をしっかり確認したいときは?

Chrome の DevTools を使えば、ネットワーク速度を意図的に遅くしてローディングUIの挙動を確認できます。

手順:

  1. Chromeで対象ページ(例:/articles)を開く
  2. DevTools を開き、Network タブへ
  3. 上部の No throttlingSlow 4G3G に変更
  4. ページをリロードして、ローディング表示のタイミングやアニメーションの流れを確認

💡 実際のユーザーは通信状況がさまざまなので、早い環境だけで開発・確認しないのはとても大事です!

まとめ

この記事では、Next.js App Routerにおけるローディング表示の実装を3段階に分けて比較しながら紹介しました。

Step 特徴 ローディング表示 描画タイミング 対応技術
1 ローディング未実装 ❌ なし 一括表示 なし
2 loading.tsx による画面全体の表示 ✅ 画面中央 一括表示 loading.tsx + async page.tsx
3 <Suspense> による部分的ローディング ✅ スケルトン表示 順次表示 <Suspense> + fallback

それぞれのステップで得られた学び:

  • loading.tsx の活用で「待っていること」が明示できるようになる
  • <Suspense> を使えば、完了した部分から順に表示され、待たせない体験を実現できる
  • スケルトンUIのサイズ設計を工夫することで、レイアウトシフトを防げる
  • Chrome DevTools でネットワーク速度を落とすことで、ローディング挙動の確認がしやすい

UXを意識したフロントエンド開発では、「表示が遅い=悪い」ではなく、「ユーザーにとって自然で安心できる表示」にすることが重要だと改めて感じました。

今回作成したコードはこちら

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?