9
章節9
串流
在前一章中,您學習了 Next.js 的不同渲染方法。我們也討論了緩慢的資料提取如何影響應用程式的效能。讓我們看看在有緩慢的資料請求時,您可以如何改善使用者體驗。
在本章中...
以下是我們將涵蓋的主題
什麼是串流,以及您可能在何時使用它。
如何使用 loading.tsx
和 Suspense 實作串流。
什麼是載入骨架。
什麼是 Next.js 路由群組,以及您可能在何時使用它們。
在應用程式中何處放置 React Suspense 邊界。
什麼是串流?
串流是一種資料傳輸技術,可讓您將路由分解為較小的「區塊」,並在區塊準備就緒時,逐步從伺服器串流到用戶端。


透過串流,您可以防止緩慢的資料請求封鎖整個頁面。這讓使用者可以在不等待所有資料載入完成才顯示任何 UI 的情況下,查看頁面的一部分並與之互動。


串流與 React 的組件模型配合良好,因為每個組件都可以被視為一個區塊。
在 Next.js 中,有兩種實作串流的方法
- 在頁面層級,使用
loading.tsx
檔案(它會為您建立<Suspense>
)。 - 在組件層級,使用
<Suspense>
以獲得更精細的控制。
讓我們看看這是如何運作的。
使用 loading.tsx
串流整個頁面
在 /app/dashboard
資料夾中,建立一個名為 loading.tsx
的新檔案
export default function Loading() {
return <div>Loading...</div>;
}
重新整理 https://127.0.0.1:3000/dashboard,您現在應該會看到


這裡發生了一些事情
loading.tsx
是一個特殊的 Next.js 檔案,建立在 React Suspense 之上。它讓您可以建立後備 UI,在頁面內容載入時顯示為替代方案。- 由於
<SideNav>
是靜態的,因此會立即顯示。使用者可以在動態內容載入時與<SideNav>
互動。 - 使用者不必等待頁面完成載入才能離開導航(這稱為可中斷的導航)。
恭喜!您剛剛實作了串流。但是我們可以做更多的事情來改善使用者體驗。讓我們顯示載入骨架,而不是 Loading…
文字。
新增載入骨架
載入骨架是 UI 的簡化版本。許多網站使用它們作為佔位符(或後備方案)來向使用者指示內容正在載入中。您在 loading.tsx
中新增的任何 UI 都將作為靜態檔案的一部分嵌入,並首先傳送。然後,其餘的動態內容將從伺服器串流到用戶端。
在您的 loading.tsx
檔案中,匯入一個名為 <DashboardSkeleton>
的新組件
import DashboardSkeleton from '@/app/ui/skeletons';
export default function Loading() {
return <DashboardSkeleton />;
}
然後,重新整理 https://127.0.0.1:3000/dashboard,您現在應該會看到


使用路由群組修正載入骨架錯誤
目前,您的載入骨架將套用至發票。
由於 loading.tsx
在檔案系統中的層級高於 /invoices/page.tsx
和 /customers/page.tsx
,因此它也適用於這些頁面。
我們可以透過路由群組來變更此行為。在 dashboard 資料夾內建立一個名為 /(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>
組件以提取自己的資料,並移除傳遞給它的 prop
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>
組件。使用名為 <LatestInvoicesSkeleton>
的後備方案將組件包裝在 <Suspense>
邊界中。
準備就緒後,展開切換按鈕以查看解決方案程式碼
群組化組件
太棒了!您快完成了,現在您需要將 <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 渲染模型,其設計考慮了串流。
這有幫助嗎?