Streaming

Im vorherigen Kapitel haben Sie die verschiedenen Rendering-Methoden von Next.js kennengelernt. Wir haben auch besprochen, wie langsame Datenabfragen die Leistung Ihrer Anwendung beeinträchtigen können. Schauen wir uns an, wie Sie die Benutzererfahrung bei langsamen Datenanfragen verbessern können.

Was ist Streaming?

Streaming ist eine Datenübertragungstechnik, die es ermöglicht, eine Route in kleinere "Chunks" aufzuteilen und diese schrittweise vom Server zum Client zu streamen, sobald sie bereit sind.

Diagramm, das die Zeit mit sequenzieller und paralleler Datenabfrage zeigt

Durch Streaming können Sie verhindern, dass langsame Datenanfragen Ihre gesamte Seite blockieren. Dies ermöglicht es dem Benutzer, Teile der Seite zu sehen und mit ihnen zu interagieren, ohne darauf warten zu müssen, dass alle Daten geladen sind, bevor eine Benutzeroberfläche angezeigt wird.

Diagramm, das die Zeit mit sequenzieller und paralleler Datenabfrage zeigt

Streaming funktioniert gut mit dem Komponentenmodell von React, da jede Komponente als ein Chunk betrachtet werden kann.

Es gibt zwei Möglichkeiten, Streaming in Next.js zu implementieren:

  1. Auf Seitenebene mit der Datei loading.tsx (die automatisch <Suspense> für Sie erstellt).
  2. Auf Komponentenebene mit <Suspense> für eine feinere Kontrolle.

Sehen wir uns an, wie das funktioniert.

Streaming einer gesamten Seite mit loading.tsx

Erstellen Sie im Ordner /app/dashboard eine neue Datei namens loading.tsx:

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

Aktualisieren Sie http://localhost:3000/dashboard, und Sie sollten nun Folgendes sehen:

Dashboard-Seite mit 'Loading...'-Text

Hier passieren mehrere Dinge:

  1. loading.tsx ist eine spezielle Next.js-Datei, die auf React Suspense basiert. Sie ermöglicht es Ihnen, eine Fallback-UI zu erstellen, die während des Ladens der Seiteninhalte angezeigt wird.
  2. Da <SideNav> statisch ist, wird es sofort angezeigt. Der Benutzer kann mit <SideNav> interagieren, während die dynamischen Inhalte geladen werden.
  3. Der Benutzer muss nicht warten, bis die Seite vollständig geladen ist, um wegzunavigieren (dies wird als unterbrechbare Navigation bezeichnet).

Glückwunsch! Sie haben gerade Streaming implementiert. Aber wir können noch mehr tun, um die Benutzererfahrung zu verbessern. Lassen Sie uns anstelle des Loading…-Textes ein Lade-Skelett anzeigen.

Hinzufügen von Lade-Skeletten

Ein Lade-Skelett ist eine vereinfachte Version der Benutzeroberfläche. Viele Websites verwenden sie als Platzhalter (oder Fallback), um den Benutzern anzuzeigen, dass Inhalte geladen werden. Jede UI, die Sie in loading.tsx hinzufügen, wird als Teil der statischen Datei eingebettet und zuerst gesendet. Anschließend werden die restlichen dynamischen Inhalte vom Server zum Client gestreamt.

Importieren Sie in Ihrer loading.tsx-Datei eine neue Komponente namens <DashboardSkeleton>:

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

Aktualisieren Sie dann http://localhost:3000/dashboard, und Sie sollten nun Folgendes sehen:

Dashboard-Seite mit Lade-Skeletten

Beheben des Lade-Skelett-Bugs mit Route Groups

Derzeit gilt Ihr Lade-Skelett auch für die Rechnungen.

Da loading.tsx im Dateisystem eine Ebene höher liegt als /invoices/page.tsx und /customers/page.tsx, wird es auch auf diese Seiten angewendet.

Wir können dies mit Route Groups ändern. Erstellen Sie einen neuen Ordner namens /(overview) innerhalb des Dashboard-Ordners. Verschieben Sie dann Ihre loading.tsx- und page.tsx-Dateien in diesen Ordner:

Ordnerstruktur, die zeigt, wie eine Route Group mit Klammern erstellt wird

Jetzt gilt die loading.tsx-Datei nur noch für Ihre Dashboard-Übersichtsseite.

Route Groups ermöglichen es Ihnen, Dateien in logische Gruppen zu organisieren, ohne die URL-Pfadstruktur zu beeinflussen. Wenn Sie einen neuen Ordner mit Klammern () erstellen, wird der Name nicht in den URL-Pfad aufgenommen. So wird /dashboard/(overview)/page.tsx zu /dashboard.

Hier verwenden Sie eine Route Group, um sicherzustellen, dass loading.tsx nur für Ihre Dashboard-Übersichtsseite gilt. Sie können Route Groups jedoch auch verwenden, um Ihre Anwendung in Abschnitte (z. B. (marketing)-Routen und (shop)-Routen) oder nach Teams für größere Anwendungen zu unterteilen.

Streaming einer Komponente

Bisher streamen Sie eine ganze Seite. Sie können jedoch auch spezifische Komponenten mit React Suspense streamen.

Suspense ermöglicht es Ihnen, das Rendern von Teilen Ihrer Anwendung zu verzögern, bis eine bestimmte Bedingung erfüllt ist (z. B. Daten geladen sind). Sie können Ihre dynamischen Komponenten in Suspense einwickeln. Übergeben Sie dann eine Fallback-Komponente, die angezeigt wird, während die dynamische Komponente lädt.

Wenn Sie sich an die langsame Datenanfrage fetchRevenue() erinnern, ist dies die Anfrage, die die gesamte Seite verlangsamt. Anstatt Ihre gesamte Seite zu blockieren, können Sie Suspense verwenden, um nur diese Komponente zu streamen und den Rest der Benutzeroberfläche der Seite sofort anzuzeigen.

Dazu müssen Sie die Datenabfrage in die Komponente verschieben. Lassen Sie uns den Code aktualisieren, um zu sehen, wie das aussehen wird:

Löschen Sie alle Instanzen von fetchRevenue() und deren Daten aus /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'; // entfernen Sie fetchRevenue
 
export default async function Page() {
  const revenue = await fetchRevenue() // löschen Sie diese Zeile
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  return (
    // ...
  );
}

Importieren Sie dann <Suspense> von React und wickeln Sie es um <RevenueChart />. Sie können ihm eine Fallback-Komponente namens <RevenueChartSkeleton> übergeben.

/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>
  );
}

Aktualisieren Sie schließlich die <RevenueChart>-Komponente, um ihre eigenen Daten abzurufen, und entfernen Sie die übergebene Prop:

/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() { // Machen Sie die Komponente async, entfernen Sie die Props
  const revenue = await fetchRevenue(); // Daten innerhalb der Komponente abrufen
 
  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 (
    // ...
  );
}
 

Aktualisieren Sie nun die Seite. Sie sollten die Dashboard-Informationen fast sofort sehen, während ein Fallback-Skelett für <RevenueChart> angezeigt wird:

Dashboard-Seite mit Revenue-Chart-Skelett und geladenen Card- und Latest-Invoices-Komponenten

Übung: Streaming von <LatestInvoices>

Jetzt sind Sie an der Reihe! Üben Sie, was Sie gerade gelernt haben, indem Sie die <LatestInvoices>-Komponente streamen.

Verschieben Sie fetchLatestInvoices() von der Seite in die <LatestInvoices>-Komponente. Wickeln Sie die Komponente in eine <Suspense>-Grenze mit einem Fallback namens <LatestInvoicesSkeleton>.

Wenn Sie bereit sind, erweitern Sie den Toggle, um den Lösungscode zu sehen:

Gruppieren von Komponenten

Großartig! Sie sind fast fertig. Jetzt müssen Sie die <Card>-Komponenten in Suspense einwickeln. Sie könnten Daten für jede einzelne Karte abrufen, aber dies könnte zu einem Popping-Effekt führen, wenn die Karten nacheinander laden, was für den Benutzer visuell störend sein kann.

Wie würden Sie dieses Problem angehen?

Um einen eher gestaffelten Effekt zu erzielen, können Sie die Karten mit einer Wrapper-Komponente gruppieren. Dies bedeutet, dass zuerst die statische <SideNav/> angezeigt wird, gefolgt von den Karten usw.

In Ihrer page.tsx-Datei:

  1. Löschen Sie Ihre <Card>-Komponenten.
  2. Löschen Sie die fetchCardData()-Funktion.
  3. Importieren Sie eine neue Wrapper-Komponente namens <CardWrapper />.
  4. Importieren Sie ein neues Skeleton-Komponente namens <CardsSkeleton />.
  5. Wickeln Sie <CardWrapper /> in Suspense.
/app/dashboard/(overview)/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>
  );
}

Wechseln Sie dann in die Datei /app/ui/dashboard/cards.tsx, importieren Sie die fetchCardData()-Funktion und rufen Sie sie innerhalb der <CardWrapper/>-Komponente auf. Stellen Sie sicher, dass Sie den notwendigen Code in dieser Komponente entkommentieren.

/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"
      />
    </>
  );
}

Aktualisieren Sie die Seite, und Sie sollten sehen, dass alle Karten gleichzeitig laden. Sie können dieses Muster verwenden, wenn mehrere Komponenten gleichzeitig laden sollen.

Entscheiden, wo Sie Ihre Suspense-Grenzen platzieren

Wo Sie Ihre Suspense-Grenzen platzieren, hängt von einigen Faktoren ab:

  1. Wie der Benutzer die Seite während des Streamings erleben soll.
  2. Welche Inhalte Sie priorisieren möchten.
  3. Ob die Komponenten auf Datenabfragen angewiesen sind.

Schauen Sie sich Ihre Dashboard-Seite an. Gibt es etwas, das Sie anders gemacht hätten?

Machen Sie sich keine Sorgen. Es gibt keine richtige Antwort.

  • Sie könnten die gesamte Seite streamen, wie wir es mit loading.tsx getan haben... aber das könnte zu längeren Ladezeiten führen, wenn eine der Komponenten eine langsame Datenabfrage hat.
  • Sie könnten jede Komponente einzeln streamen... aber das könnte dazu führen, dass die Benutzeroberfläche nacheinander auf dem Bildschirm erscheint, sobald sie bereit ist.
  • Sie könnten auch einen gestaffelten Effekt erzielen, indem Sie Seitenabschnitte streamen. Dafür müssen Sie jedoch Wrapper-Komponenten erstellen.

Wo Sie Ihre Suspense-Grenzen platzieren, hängt von Ihrer Anwendung ab. Im Allgemeinen ist es eine gute Praxis, Ihre Datenabfragen in die Komponenten zu verschieben, die sie benötigen, und diese Komponenten dann in Suspense zu wickeln. Aber es ist nichts falsch daran, Abschnitte oder die gesamte Seite zu streamen, wenn Ihre Anwendung dies erfordert.

Scheuen Sie sich nicht, mit Suspense zu experimentieren und zu sehen, was am besten funktioniert. Es ist eine leistungsstarke API, die Ihnen helfen kann, ansprechendere Benutzererlebnisse zu schaffen.

Ausblick

Streaming und Server Components bieten uns neue Möglichkeiten, Datenabfragen und Ladezustände zu handhaben, mit dem ultimativen Ziel, das Benutzererlebnis zu verbessern.

Im nächsten Kapitel lernen Sie Partial Prerendering kennen, ein neues Next.js-Rendering-Modell, das mit Streaming im Hinterkopf entwickelt wurde.