コンテンツにスキップ

9

ストリーミング

前のチャプターでは、Next.jsの異なるレンダリング方法について学びました。また、遅いデータフェッチがアプリケーションのパフォーマンスにどのように影響するかについても説明しました。データリクエストが遅い場合に、ユーザーエクスペリエンスを向上させる方法を見てみましょう。

このチャプターでは...

以下のトピックについて説明します

ストリーミングとは何か、そしていつ使用するのか。

loading.tsx と Suspense を使用してストリーミングを実装する方法。

ローディングスケルトンとは何か。

ルートグループとは何か、そしていつ使用するのか。

アプリケーションのどこに Suspense 境界を配置するのか。

ストリーミングとは?

ストリーミングとは、ルートを小さな「チャンク」に分割し、サーバーからクライアントに準備が整った時点で progressivly にストリーミングするデータ転送技術です。

Diagram showing time with sequential data fetching and parallel data fetching

ストリーミングを行うことで、遅いデータリクエストがページ全体をブロックするのを防ぐことができます。これにより、すべてのデータがロードされるまで待たずに、ページの一部を表示して操作できるようになります。

Diagram showing time with sequential data fetching and parallel data fetching

ストリーミングは、各コンポーネントを*チャンク*と見なすことができるため、React のコンポーネントモデルと相性が良いです。

Next.js でストリーミングを実装するには、2つの方法があります。

  1. ページレベルでは、loading.tsx ファイルを使用します。
  2. 特定のコンポーネントの場合は、<Suspense> を使用します。

どのように動作するか見てみましょう。

loading.tsx でページ全体をストリーミングする

/app/dashboard フォルダーに、loading.tsx という新しいファイルを作成します。

/app/dashboard/loading.tsx
export default function Loading() {
  return <div>Loading...</div>;
}

http://localhost:3000/dashboard を更新すると、次のように表示されます。

Dashboard page with 'Loading...' text

ここでいくつかのことが起こっています

  1. loading.tsx は Suspense をベースに構築された特別な Next.js ファイルであり、ページコンテンツの読み込み中に代替として表示するフォールバック UI を作成できます。
  2. <SideNav> は静的であるため、すぐに表示されます。動的コンテンツの読み込み中に、ユーザーは <SideNav> を操作できます。
  3. ユーザーは、ページの読み込みが完了するまで待たずに、移動できます(これは中断可能なナビゲーションと呼ばれます)。

おめでとうございます!ストリーミングを実装しました。ただし、ユーザーエクスペリエンスを向上させるために、さらにできることがあります。Loading… テキストの代わりにローディングスケルトンを表示してみましょう。

ローディングスケルトンの追加

ローディングスケルトンとは、UI の簡略化されたバージョンです。多くのウェブサイトでは、コンテンツがロードされていることをユーザーに示すプレースホルダー(またはフォールバック)として使用されています。loading.tsx に追加した UI は、静的ファイルの一部として埋め込まれ、最初に送信されます。その後、残りの動的コンテンツがサーバーからクライアントにストリーミングされます。

loading.tsx ファイル内で、<DashboardSkeleton> と呼ばれる新しいコンポーネントをインポートします。

/app/dashboard/loading.tsx
import DashboardSkeleton from '@/app/ui/skeletons';
 
export default function Loading() {
  return <DashboardSkeleton />;
}

次に、http://localhost:3000/dashboard を更新すると、次のように表示されます。

Dashboard page with loading skeletons

ルートグループを使用してローディングスケルトンのバグを修正する

現在、ローディングスケルトンは、請求書と顧客のページにも適用されます。

ファイルシステムでは、loading.tsx/invoices/page.tsx および /customers/page.tsx よりも上位にあるため、これらのページにも適用されます。

ルートグループ を使用して、これを変更できます。ダッシュボードフォルダー内に /(overview) という新しいフォルダーを作成します。次に、loading.tsxpage.tsx ファイルをフォルダー内に移動します。

Folder structure showing how to create a route group using parentheses

これで、loading.tsx ファイルはダッシュボードの概要ページにのみ適用されます。

ルートグループを使用すると、URL パス構造に影響を与えることなく、ファイルを論理グループに整理できます。括弧 () を使用して新しいフォルダーを作成すると、その名前は URL パスに含まれません。そのため、/dashboard/(overview)/page.tsx/dashboard になります。

ここでは、ルートグループを使用して、loading.tsx がダッシュボードの概要ページにのみ適用されるようにしています.ただし、ルートグループを使用して、アプリケーションをセクション(例:(marketing) ルートと (shop) ルート)に分割したり、大規模なアプリケーションの場合はチームごとに分割したりすることもできます。

コンポーネントのストリーミング

これまでは、ページ全体をストリーミングしていました.ただし、React Suspense を使用して、特定のコンポーネントをよりきめ細かくストリーミングすることもできます。

Suspense を使用すると、特定の条件(例:データの読み込み)が満たされるまで、アプリケーションの一部のレンダリングを延期できます。動的コンポーネントを Suspense でラップできます。次に、動的コンポーネントの読み込み中に表示するフォールバックコンポーネントを渡します.

遅いデータリクエストである fetchRevenue() を覚えている場合、これはページ全体を遅くしているリクエストです。ページ全体をブロックする代わりに、Suspense を使用してこのコンポーネントのみをストリーミングし、ページの残りの UI をすぐに表示できます。

そのためには、データフェッチをコンポーネントに移動する必要があります。コードを更新して、それがどのように見えるかを見てみましょう。

fetchRevenue() とそのデータのすべてのインスタンスを /dashboard/(overview)/page.tsx から削除します。

/app/dashboard/(overview)/page.tsx
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> と呼ばれるフォールバックコンポーネントを渡すことができます。

/app/dashboard/(overview)/page.tsx
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> コンポーネントを更新して独自のデータを取得し、渡されたプロパティを削除します。

/app/ui/dashboard/revenue-chart.tsx
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> のフォールバックスケルトンが表示されている間に、ダッシュボードの情報がほぼ瞬時に表示されます。

Dashboard page with revenue chart skeleton and loaded Card and Latest Invoices components

練習:<LatestInvoices> のストリーミング

今度はあなたの番です!学んだことを実践して、<LatestInvoices> コンポーネントをストリーミングしてみましょう。

fetchLatestInvoices() をページから <LatestInvoices> コンポーネントに移動します。コンポーネントを <Suspense> 境界でラップし、フォールバックとして <LatestInvoicesSkeleton> を使用します。

準備ができたら、トグルを展開してソリューションコードを確認してください。

コンポーネントのグループ化

素晴らしい!あと少しです。次は、<Card> コンポーネントを Suspense でラップする必要があります。各カードのデータを個別にフェッチすることもできますが、カードが読み込まれる際に*ポップ*するような効果が発生し、ユーザーにとって視覚的に不 jarring な場合があります。

では、この問題にどのように対処すればよいでしょうか?

より*段階的な*効果を得るには、ラッパーコンポーネントを使用してカードをグループ化します。つまり、静的な <SideNav/> が最初に表示され、次にカードなどが表示されます。

page.tsx ファイルで

  1. <Card> コンポーネントを削除します。
  2. fetchCardData() 関数を削除します。
  3. <CardWrapper /> という新しい**ラッパー**コンポーネントをインポートします。
  4. <CardsSkeleton /> という新しい**スケルトン**コンポーネントをインポートします。
  5. <CardWrapper /> を Suspense でラップします。
/app/dashboard/page.tsx
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/> コンポーネント内で呼び出します。このコンポーネントで必要なコードのコメントを外してください。

/app/ui/dashboard/cards.tsx
// ...
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 境界をどこに配置するかは、いくつかの要素によって異なります。

  1. ページがストリーム配信される際のユーザーエクスペリエンスをどのようにしたいか。
  2. どのコンテンツを優先したいか。
  3. コンポーネントがデータフェッチに依存しているかどうか。

ダッシュボードページを見て、何か違うことをしたでしょうか?

心配しないでください。正解はありません。

  • loading.tsx のように、**ページ全体**をストリーム配信することもできます...しかし、いずれかのコンポーネントのデータフェッチが遅い場合、読み込み時間が長くなる可能性があります。
  • **すべてのコンポーネント**を個別にストリーム配信することもできます...しかし、準備ができ次第 UI が画面に*ポップ*する可能性があります。
  • **ページセクション**をストリーム配信することで、*段階的な*効果を作成することもできます。ただし、ラッパーコンポーネントを作成する必要があります。

Suspense 境界をどこに配置するかは、アプリケーションによって異なります。一般的には、データフェッチを必要とするコンポーネントに移動し、それらのコンポーネントを Suspense でラップすることをお勧めします.それでも、アプリケーションのニーズに合わせてセクションまたはページ全体をストリーミング配信しても問題ありません.

Suspense を試して、何が最適かを確認することを恐れないでください。これは、より快適なユーザーエクスペリエンスを作成するのに役立つ強力な API です。

今後の展望

ストリーミングとサーバーコンポーネントは、データフェッチと読み込み状態を処理する新しい方法を提供し、最終的にはエンドユーザーエクスペリエンスの向上を目的としています。

次の章では、ストリーミングを念頭に置いて構築された新しい Next.js レンダリングモデルである部分プリレンダリングについて学習します。

チャプター完了9

Suspense と読み込みスケルトンを使用してコンポーネントをストリーム配信する方法を学習しました。

次へ

10: 部分プリレンダリング

部分プリレンダリングの初期段階 - ストリーミングで構築された新しい実験的レンダリングモデル。