9
チャプター9
ストリーミング
前章では、Next.js のさまざまなレンダリング方法について学びました。また、データ取得が遅いとアプリケーションのパフォーマンスにどのように影響するかも説明しました。データ要求が遅い場合にユーザーエクスペリエンスを向上させる方法を見ていきましょう。
このチャプターでは...
取り上げるトピックは次のとおりです
ストリーミングとは何か、そしていつ使用するか。
loading.tsx
と Suspense を使用してストリーミングを実装する方法。
ローディングスケルトンとは何か。
Next.js のルーティンググループとは何か、そしていつ使用するか。
アプリケーション内で React Suspense の境界をどこに配置するか。
ストリーミングとは?
ストリーミングとは、ルートをより小さな「チャンク」に分割し、準備ができたものから順にサーバーからクライアントへ段階的にストリームできるデータ転送技術です。

ストリーミングを使用することで、遅いデータ要求がページ全体をブロックするのを防ぐことができます。これにより、すべてのデータが読み込まれるのを待たずに、ユーザーはページの一部を表示し、操作できるようになります。

ストリーミングは React のコンポーネントモデルとうまく機能します。各コンポーネントをチャンクとみなすことができるためです。
Next.js でストリーミングを実装する方法は2つあります
- ページレベルでは、
loading.tsx
ファイルを使用します(これは自動的に<Suspense>
を作成します)。 - コンポーネントレベルでは、よりきめ細かな制御のために
<Suspense>
を使用します。
それでは、どのように機能するか見ていきましょう。
loading.tsx
でページ全体をストリーミングする
/app/dashboard
フォルダに、loading.tsx
という新しいファイルを作成します。
export default function Loading() {
return <div>Loading...</div>;
}
https://:3000/dashboard をリフレッシュすると、次のように表示されるはずです

ここではいくつかのことが起こっています
loading.tsx
は、React Suspense の上に構築された特別な Next.js ファイルです。ページのコンテンツが読み込まれている間に、代替として表示するフォールバック UI を作成できます。<SideNav>
は静的なので、すぐに表示されます。ユーザーは動的なコンテンツが読み込まれている間でも<SideNav>
を操作できます。- ユーザーは、ページが完全に読み込まれるのを待たずに別のページに移動できます(これは中断可能なナビゲーションと呼ばれます)。
おめでとうございます!ストリーミングを実装できました。しかし、ユーザーエクスペリエンスを向上させるために、さらにできることがあります。Loading…
テキストの代わりにローディングスケルトンを表示してみましょう。
ローディングスケルトンの追加
ローディングスケルトンは、UI の簡略化されたバージョンです。多くのウェブサイトでは、コンテンツが読み込み中であることをユーザーに示すためのプレースホルダー(またはフォールバック)として使用されています。loading.tsx
に追加した UI は静的ファイルの一部として埋め込まれ、最初に送信されます。その後、残りの動的コンテンツがサーバーからクライアントにストリーミングされます。
loading.tsx
ファイル内で、<DashboardSkeleton>
という新しいコンポーネントをインポートします。
import DashboardSkeleton from '@/app/ui/skeletons';
export default function Loading() {
return <DashboardSkeleton />;
}
次に、https://:3000/dashboard をリフレッシュすると、次のように表示されるはずです

ルートグループでローディングスケルトンのバグを修正する
現在、ローディングスケルトンは請求書に適用されます。
loading.tsx
はファイルシステムで /invoices/page.tsx
や /customers/page.tsx
よりも上位のレベルにあるため、それらのページにも適用されます。
これは ルーティンググループ で変更できます。ダッシュボードフォルダ内に /(overview)
という新しいフォルダを作成します。次に、loading.tsx
と page.tsx
ファイルをそのフォルダの中に移動します。

これで、loading.tsx
ファイルはダッシュボードの概要ページにのみ適用されるようになります。
ルーティンググループを使用すると、URL パス構造に影響を与えることなく、ファイルを論理的なグループに整理できます。括弧 ()
を使用して新しいフォルダを作成すると、その名前は URL パスに含まれません。したがって、/dashboard/(overview)/page.tsx
は /dashboard
になります。
ここでは、ルーティンググループを使用して loading.tsx
がダッシュボードの概要ページにのみ適用されるようにしています。ただし、ルーティンググループは、アプリケーションをセクション(例:(marketing)
ルートと (shop)
ルート)に分割したり、大規模なアプリケーションではチームごとに分割したりするためにも使用できます。
コンポーネントのストリーミング
これまではページ全体をストリーミングしていましたが、React Suspense を使用して、より細かく特定のコンポーネントをストリーミングすることもできます。
Suspense を使用すると、特定の条件が満たされるまで(例:データが読み込まれるまで)アプリケーションの一部をレンダリングを遅延させることができます。動的なコンポーネントを Suspense でラップし、動的なコンポーネントが読み込まれている間に表示するフォールバックコンポーネントを渡すことができます。
遅いデータ要求である fetchRevenue()
を覚えているでしょうか。これはページ全体を遅くしている要求です。ページ全体をブロックする代わりに、Suspense を使用してこのコンポーネントのみをストリーミングし、ページの残りの UI をすぐに表示することができます。
そのためには、データ取得をコンポーネントに移動する必要があります。どのように見えるか、コードを更新してみましょう。
/dashboard/(overview)/page.tsx
から fetchRevenue()
のすべてのインスタンスとそのデータを削除します。
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data'; // remove fetchRevenue
export default async function Page() {
const revenue = await fetchRevenue() // delete this line
const latestInvoices = await fetchLatestInvoices();
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();
return (
// ...
);
}
次に、React から <Suspense>
をインポートし、<RevenueChart />
を囲みます。これに <RevenueChartSkeleton>
というフォールバックコンポーネントを渡すことができます。
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data';
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';
export default async function Page() {
const latestInvoices = await fetchLatestInvoices();
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();
return (
<main>
<h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Dashboard
</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<Card title="Collected" value={totalPaidInvoices} type="collected" />
<Card title="Pending" value={totalPendingInvoices} type="pending" />
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
<Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/>
</div>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
<LatestInvoices latestInvoices={latestInvoices} />
</div>
</main>
);
}
最後に、<RevenueChart>
コンポーネントを更新して、独自のデータを取得し、渡されたプロップを削除します。
import { generateYAxis } from '@/app/lib/utils';
import { CalendarIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
// ...
export default async function RevenueChart() { // Make component async, remove the props
const revenue = await fetchRevenue(); // Fetch data inside the component
const chartHeight = 350;
const { yAxisLabels, topLabel } = generateYAxis(revenue);
if (!revenue || revenue.length === 0) {
return <p className="mt-4 text-gray-400">No data available.</p>;
}
return (
// ...
);
}
ページをリフレッシュすると、ダッシュボードの情報がほぼすぐに表示され、<RevenueChart>
のフォールバックスケルトンが表示されるはずです。

演習: <LatestInvoices>
のストリーミング
今度はあなたの番です!学んだことを練習して、<LatestInvoices>
コンポーネントをストリーミングしてみましょう。
fetchLatestInvoices()
をページから <LatestInvoices>
コンポーネントに移動します。コンポーネントを <Suspense>
の境界で囲み、<LatestInvoicesSkeleton>
というフォールバックを渡します。
準備ができたら、トグルを展開して解答コードを表示してください
コンポーネントのグループ化
素晴らしい!あと少しです。次に、<Card>
コンポーネントを Suspense でラップする必要があります。個々のカードのデータを取得することはできますが、カードが読み込まれる際にポップアップするような効果が発生し、ユーザーにとって視覚的に不快になる可能性があります。
では、この問題をどう解決しますか?
よりずらした効果を作成するには、ラッパーコンポーネントを使用してカードをグループ化できます。これにより、静的な <SideNav/>
が最初に表示され、次にカードなどが表示されるようになります。
page.tsx
ファイルで
<Card>
コンポーネントを削除します。fetchCardData()
関数を削除します。<CardWrapper />
という新しいラッパーコンポーネントをインポートします。<CardsSkeleton />
という新しいスケルトンコンポーネントをインポートします。<CardWrapper />
を Suspense でラップします。
import CardWrapper from '@/app/ui/dashboard/cards';
// ...
import {
RevenueChartSkeleton,
LatestInvoicesSkeleton,
CardsSkeleton,
} from '@/app/ui/skeletons';
export default async function Page() {
return (
<main>
<h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Dashboard
</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<Suspense fallback={<CardsSkeleton />}>
<CardWrapper />
</Suspense>
</div>
// ...
</main>
);
}
次に、/app/ui/dashboard/cards.tsx
ファイルに移動し、fetchCardData()
関数をインポートし、<CardWrapper/>
コンポーネント内で呼び出します。このコンポーネント内で必要なコードのコメントを解除するようにしてください。
// ...
import { fetchCardData } from '@/app/lib/data';
// ...
export default async function CardWrapper() {
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();
return (
<>
<Card title="Collected" value={totalPaidInvoices} type="collected" />
<Card title="Pending" value={totalPendingInvoices} type="pending" />
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
<Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/>
</>
);
}
ページをリフレッシュすると、すべてのカードが同時に読み込まれるのが確認できるはずです。複数のコンポーネントを同時に読み込みたい場合にこのパターンを使用できます。
Suspense の境界をどこに配置するかを決定する
Suspense の境界をどこに配置するかは、いくつかの要素に依存します。
- ストリーミング中にユーザーにページをどのように体験してほしいか。
- どのコンテンツを優先したいか。
- コンポーネントがデータ取得に依存しているか。
ダッシュボードページを見て、何か違うことをしたでしょうか?
心配いりません。正解はありません。
loading.tsx
を使用したようにページ全体をストリーミングすることもできますが、コンポーネントのいずれかがデータ取得に時間がかかると、読み込み時間が長くなる可能性があります。- すべてのコンポーネントを個別にストリーミングすることもできますが、準備ができたときに UI が画面にポップアップ表示される可能性があります。
- ページセクションをストリーミングすることで、ずらした効果を作成することもできます。しかし、そのためにはラッパーコンポーネントを作成する必要があります。
Suspense の境界をどこに配置するかは、アプリケーションによって異なります。一般的には、データ取得を必要とするコンポーネントに移動し、それらのコンポーネントを Suspense でラップするのが良い習慣です。しかし、アプリケーションが必要とするものであれば、セクション全体やページ全体をストリーミングしても問題ありません。
Suspense を試して何が最適かを見つけることを恐れないでください。これは、より魅力的なユーザーエクスペリエンスを作成するのに役立つ強力な API です。
今後の展望
ストリーミングとサーバーコンポーネントは、データ取得と読み込み状態を処理する新しい方法を提供し、最終的にエンドユーザーエクスペリエンスの向上を目指します。
次の章では、ストリーミングを念頭に置いて構築された新しい Next.js レンダリングモデルである Partial Prerendering について学びます。
チャプターを完了しました9
Suspense とローディングスケルトンでコンポーネントをストリーミングする方法を学びました。
お役に立ちましたか?