【Next.js】ローディング画面の作り方を loading.tsx から初回だけのオープニングまで解説

公開日: 2026年10月8日 · 読了目安 9 分

Next.js でローディング画面を作る手順です。出すべき場面と出さない場面、App Router の loading.tsx と Suspense、スケルトンとスピナーの使い分け、セッションで1回だけ出るオープニングとフェードアウト、LCP を落とさない注意点まで、コード付きで解説します。

ローディング画面は待ち時間があるときだけ出す

Next.js でサイトを作っていて、開いた瞬間のローディング画面を入れたくなった。でも、ただ読者を待たせるだけにならないかが気になっている。

答えは次のとおりです。ローディング画面は、本当に待ち時間があるときだけ出します。

ページ移動中の待ちは、App Router の loading.tsx で出せます。ブランドの演出として見せる場合も、出すのはセッションで1回、長くても1.5秒ほどが目安です。

この記事で分かることは次のとおりです。

  • ローディング画面を出すべき場面と、出さないほうがよい場面

  • loading.tsx と Suspense で、待っている間の画面を出す方法

  • スケルトンとスピナーの使い分け

  • 初回だけ出る全画面のオープニングと、Motion でのフェードアウト

  • 表示速度の指標(LCP)を落とさないための注意点

ローディング画面を出す場面と出さない場面

ローディング画面の役目は、待ち時間に「ちゃんと動いている」と伝えることです。待ち時間がないのに出すと、ただの足止めになります。

出す場面は、主に次の3つです。

  • データの取得に時間がかかるページへ移動するとき

  • フォームの送信など、押したあとに結果を待つとき

  • ブランドサイトで、最初の1回だけ演出として見せるとき

反対に、すぐ表示できるページをわざと数秒隠すのは避けます。読者は中身を見に来ているので、待たせた時間はそのまま負担になります。

演出としてのローディング画面は、ポートフォリオやブランドサイトに限って考えるのが安全です。資料請求や購入が目的の LP では、まず中身を見せてください。

Next.js でローディング画面を作る3つの方法

目的によって、作る場所が変わります。

  • Step1: loading.tsx で、ページ移動中にスケルトンを出す

  • Step2: ボタンや一部の領域に、スピナーやスケルトンを出す

  • Step3: 初回だけ全画面のオープニングを出し、フェードアウトさせる

多くのサイトは、Step1 と Step2 で足ります。Step3 は、演出そのものがブランドの一部になる場合だけ足してください。

Step1:loading.tsx でページ移動中のローディング画面を出す

App Router では、フォルダに loading.tsx を置くだけでローディング画面になります。同じフォルダの page.tsx がデータを待つ間、代わりにこの画面が出ます。

仕組みとしては、React の Suspense(待っている間に代わりの表示を出す機能)で動いています。Next.js が、page.tsx を自動で Suspense に包んでくれます。

下の例は、商品一覧のカードと同じ形をしたスケルトンです。

app/products/loading.tsx
export default function Loading() {
  return (
    <div role="status" className="mx-auto max-w-5xl px-6 py-16">
      <span className="sr-only">商品を読み込んでいます</span>
      <div className="h-8 w-48 animate-pulse rounded-md bg-zinc-200 motion-reduce:animate-none" />
      <div className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
        {Array.from({ length: 6 }).map((_, i) => (
          <div key={i} className="space-y-3">
            <div className="aspect-[4/3] animate-pulse rounded-xl bg-zinc-200 motion-reduce:animate-none" />
            <div className="h-4 w-3/4 animate-pulse rounded bg-zinc-200 motion-reduce:animate-none" />
            <div className="h-4 w-1/2 animate-pulse rounded bg-zinc-200 motion-reduce:animate-none" />
          </div>
        ))}
      </div>
    </div>
  );
}

コードのポイント

  • role="status" と見えないテキストで、読み上げソフトにも読み込み中だと伝える

  • 本物のカードと同じ並びにして、切り替わったときのずれを小さくする

  • 点滅(animate-pulse)は、動きを減らす設定のときに止める

  • loading.tsx はサーバーで組み立てられるので、ブラウザで動かす印は要らない

ページの一部だけ待つなら Suspense で包む

ページ全体ではなく、レビューの欄だけが遅い場合もあります。そのときは、遅い部品だけを Suspense で包みます。

見出しや商品の説明は先に表示され、レビューの欄だけがスケルトンになります。

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

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  return (
    <main className="mx-auto max-w-5xl px-6 py-16">
      <h1 className="text-3xl font-semibold">商品の詳細</h1>
      <p className="mt-4 text-zinc-600">ここに商品の説明が入ります。</p>
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={id} />
      </Suspense>
    </main>
  );
}

Step2:ローディング画面のスケルトンとスピナーを使い分ける

待っている間の見せ方は、スケルトンとスピナーの2種類が基本です。

  • スケルトン:これから出る中身の形を、灰色の箱で先に見せる。一覧やカードなど、形が決まっているものに向く

  • スピナー:回る印で、処理中であることだけを伝える。ボタンの送信など、短くて形のない待ちに向く

ページ全体を中央のスピナーだけにすると、何が出てくるのか分かりません。形が分かるものは、スケルトンにしてください。

components/SubmitButton.tsx
"use client";

import { useFormStatus } from "react-dom";

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

  return (
    <button
      type="submit"
      disabled={pending}
      className="inline-flex min-h-11 items-center gap-2 rounded-lg bg-zinc-900 px-5 text-sm font-medium text-white disabled:opacity-70"
    >
      {pending && (
        <span
          aria-hidden="true"
          className="size-4 animate-spin rounded-full border-2 border-white/40 border-t-white motion-reduce:animate-none"
        />
      )}
      {pending ? "送信中…" : "送信する"}
    </button>
  );
}

送信ボタンにスピナーを出すときのポイント

useFormStatus は、親の form が送信中かどうかを教えてくれる React の機能です。form の action と組み合わせて使います。

送信中はボタンを押せなくして、二重送信を防ぎます。文字も「送信中…」に変えると、回る印が見えない人にも伝わります。

Step3:初回だけ出るオープニングのローディング画面を作る

ブランドサイトでは、ロゴが現れてから中身に切り替わる演出を入れることがあります。ただ、この全画面のローディング画面は、何度も見せると邪魔になります。

そこで、sessionStorage(タブを閉じるまで値を覚えておく仕組み)に「見た」という印を残します。同じタブでは、2回目以降は出しません。

表示は長くても1.5秒にして、スキップのボタンも置きます。消えるときは、Motion でフェードアウトさせます。

components/Splash.tsx
"use client";

import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";

const STORAGE_KEY = "splash-seen";
const MAX_MS = 1500;

function markSeen() {
  try {
    sessionStorage.setItem(STORAGE_KEY, "1");
  } catch {}
}

export function Splash() {
  const [visible, setVisible] = useState(true);

  useEffect(() => {
    let seen = false;
    try {
      seen = sessionStorage.getItem(STORAGE_KEY) === "1";
    } catch {}
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (seen || reduce) {
      setVisible(false);
      return;
    }
    const timer = window.setTimeout(() => {
      markSeen();
      setVisible(false);
    }, MAX_MS);
    return () => window.clearTimeout(timer);
  }, []);

  const skip = () => {
    markSeen();
    setVisible(false);
  };

  return (
    <AnimatePresence>
      {visible && (
        <motion.div
          key="splash"
          exit={{ opacity: 0 }}
          transition={{ duration: 0.5, ease: "easeOut" }}
          className="fixed inset-0 z-50 grid place-items-center bg-zinc-950 text-white [.splash-seen_&]:hidden"
        >
          <motion.p
            initial={{ opacity: 0, y: 12 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.6, ease: "easeOut" }}
            className="text-3xl font-semibold tracking-tight"
          >
            Acme Studio
          </motion.p>
          <button
            type="button"
            onClick={skip}
            className="absolute bottom-8 right-6 min-h-11 rounded-full px-5 text-sm text-white/70 hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-white"
          >
            スキップ
          </button>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

2回目以降のローディング画面をちらつかせない

Splash はブラウザで動く部品なので、印を確かめる前に一瞬だけ画面に出てしまいます。これを防ぐため、描画の前に動く小さなスクリプトを layout.tsx に置きます。

印がある場合や動きを減らす設定の場合は、html に splash-seen というクラスを付けます。Splash の [.splash-seen_&]:hidden が、このクラスを見て最初から隠します。

スクリプトが html の class を書き換えるので、html には suppressHydrationWarning を付けています。

app/layout.tsx
import type { ReactNode } from "react";
import { Splash } from "@/components/Splash";
import "./globals.css";

const splashScript =
  'try{if(sessionStorage.getItem("splash-seen")==="1"||matchMedia("(prefers-reduced-motion: reduce)").matches){document.documentElement.classList.add("splash-seen")}}catch(e){}';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: splashScript }} />
      </head>
      <body>
        <Splash />
        {children}
      </body>
    </html>
  );
}

コードのポイント

  • 「見た」印は、時間切れかスキップのときに保存する。開発中に処理が2回動いても、1回目として正しく扱える

  • 動きを減らす設定の人には、オープニングを最初から出さない

  • 本体のページは下で普通に描画しておき、透明にしない

  • AnimatePresence で包むと、消えるときにも exit のアニメーションが動く

Motion の基本は、https://design-layer.com/ja/articles/nextjs-framer-motion-guide で解説しています。

ローディング画面で LCP を落とさないための注意点

LCP は、ページでいちばん大きな画像や文章が表示されるまでの時間です。Google が重視する、表示速度の指標の1つです。

全画面のローディング画面は、この数値を悪くすることがあります。次の3点を守ってください。

  • 本体の文章や画像を、opacity: 0 で隠したまま待たない。透明な要素は LCP の対象にならず、計測が後ろに延びる

  • 最低表示時間のような、わざと待たせるタイマーを入れない

  • オープニングの中に、大きな画像や重い動画を置かない

演出は本体の読み込みを止めず、上に重ねるだけにします。重ねている間も、下ではページの準備が進みます。

動きを減らす設定のときのローディング画面

点滅や回転は、動きを減らす設定のときに止めるか、ゆっくりにします。Tailwind では motion-reduce: を付けると、その設定のときだけ別の指定にできます。

回る印を止める場合は、「読み込み中」や「送信中…」の文字を必ず添えてください。

ローディング画面でよくある失敗と公開前チェック

  • 待ち時間のないページに、演出のためだけのローディング画面を出す

  • ページを移動するたびに、全画面のオープニングを出す

  • スキップの手段がなく、終わるまで何もできない

  • ページ全体を中央のスピナーだけにして、何が出るか分からない

  • 読み込み中であることが、読み上げソフトに伝わらない

  • ページ全体のファイルに、ブラウザで動かす印を書いてしまう

公開前には、ブラウザの開発者ツールで回線を遅くして確かめます。Network タブで Slow 4G などを選ぶと、遅い回線での待ち時間に近い状態で見られます。

ページを開いたあとの最初の見た目は、https://design-layer.com/ja/articles/nextjs-animated-hero-section-guide で解説しています。

完成したローディング画面をコピーして使う方法

DesignLayer の「ピクセルアーク・ローダー」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「ピクセルアーク・ローダー」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

ロゴの文字が1文字ずつ波打つような、凝ったローディング画面を作りたい場合もあります。文字ごとの動きと背景の重なりを整えるのは、意外と手間がかかります。

DesignLayer のカタログにある「ピクセルアーク・ローダー」は、全画面のローディング画面です。ぼかした風景写真の上で、アーチ状に並んだ立体的なピクセルのロゴが文字ごとに波打ちます。

詳細ページのライブプレビューで動きを確かめ、「Cursor 用にコピー」を押します。組み込みの指示・ライブプレビューの URL・本体の TSX・必要な npm パッケージが、まとめてコピーされます。

Cursor や Claude に貼れば、自分の Next.js に組み込めます。そのあとは、ロゴの文言と背景写真を差し替えます。

組み込むときは、Step3 の sessionStorage の仕組みと組み合わせると、初回だけ表示できます。

閲覧とライブプレビューは無料で、アカウントもカードも要りません。無料のソースコピーは、日本時間で1日3回までです。https://design-layer.com/ja/components

ローディング画面についてよくある質問

  • 同じフォルダの page.tsx が、データを待っている間に表示されます。待ちがなければ、ほとんど見えずに切り替わります。

まとめ:Next.js でローディング画面を作る手順

  • ローディング画面は、本当に待ち時間があるときだけ出す

  • ページ移動中の待ちは、loading.tsx にスケルトンを置く

  • 一部だけ遅いなら、その部品だけを Suspense で包む

  • 形が決まっているものはスケルトン、短い送信はスピナーにする

  • オープニングはセッションで1回、1.5秒以内にして、スキップを置く

  • 本体は透明にせず、演出は上に重ねるだけにする

まずは、データを取得しているページのフォルダに loading.tsx を1つ置いてみてください。

ロゴを Lottie で動かす場合は、https://design-layer.com/ja/articles/nextjs-lottie-animation-guide で読み込みを軽くする方法を解説しています。

【Next.js】ローディング画面の作り方を loading.tsx から初回だけのオープニングまで解説 | DesignLayer