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

ストリーミングにより、遅いデータ取得がページ全体をブロックするのを防ぐことができます。これにより、ユーザーは UI が表示される前にすべてのデータがロードされるのを待つことなく、ページの各部分を表示したり操作したりできます。

ストリーミングは 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 よりも 1 レベル上にあるため、これらのページにも適用されます。
これを ルートグループ で変更できます。ダッシュボードフォルダ内に /(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 とローディングスケルトンを使用したコンポーネントのストリーミング方法を学びました。
役に立ちましたか?





