Next.js 30 天全端實戰:Day 24 - 效能與體驗:實作分頁查詢與無限滾動
一、 前言
當資料庫裡的資料多到一定程度,一次性抓取(fetch)會導致兩個問題:
- 後端崩潰:資料庫查詢太久,甚至導致記憶體溢位。
- 前端卡死:瀏覽器一次渲染數千個 DOM 元件會非常吃力。
在 Next.js 中,我們可以透過 URL 參數(Query Params)來實現傳統分頁,或是結合 Server Actions 與 Client Component 實作無限滾動。今天我們就來拆解這兩種主流做法。
二、 本文:資料分段處理實戰
1. 基於 URL 的傳統分頁 (Offset Pagination)
這是最常見的做法,優點是 SEO 友善,且使用者可以透過連結分享特定的頁碼。
// src/app/posts/page.tsx
import { db } from "@/lib/db";
export default async function PostsPage({
searchParams,
}: {
searchParams: { page?: string };
}) {
const page = Number(searchParams.page) || 1;
const pageSize = 10;
const posts = await db.post.findMany({
skip: (page - 1) * pageSize, // 跳過前面的資料
take: pageSize, // 只抓取 10 筆
orderBy: { createdAt: 'desc' },
});
return (
<div>
{/* 渲染文章列表... */}
<PaginationControls currentPage={page} />
</div>
);
}
2. 無限滾動 (Cursor-based Pagination)
對於社群媒體或移動端應用,無限滾動體驗更好。為了效能,我們通常使用「游標 (Cursor)」而非「偏移量 (Offset)」,這樣即使資料量很大,查詢效率依然穩定。
[實作邏輯]
- 初始頁面由 Server Component 渲染前 10 筆。
- Client Component 監聽滾動事件(或使用 Intersection Observer)。
- 當觸發邊界時,調用一個 Server Action 抓取下一頁資料。
- 使用
useState將新舊資料合併。
3. 使用 Intersection Observer 優化體驗
不要去監聽 window.onscroll,那太耗效能。我們在列表底部放一個「感應元件」,當它出現在視窗中時才觸發載入。
"use client";
import { useEffect, useState } from "react";
import { useInView } from "react-intersection-observer"; // 推薦套件
export function InfinitePostList({ initialPosts }) {
const [posts, setPosts] = useState(initialPosts);
const { ref, inView } = useInView();
useEffect(() => {
if (inView) {
// 呼叫 Server Action 抓取更多並 setPosts
}
}, [inView]);
return (
<div>
{posts.map(post => <PostCard key={post.id} data={post} />)}
<div ref={ref}>載入中...</div>
</div>
);
}
三、 結論:根據場景選擇正確的武器
分頁不只是 UI 議題,更是後端架構的體現。
-
今日小結:
- Offset Pagination:適合後台管理系統、需要精確跳頁的場景。
- Cursor Pagination:適合資料頻繁變動、無限滾動的場景(避免跳頁漏資料)。
- 務必在資料庫欄位(如
createdAt或id)建立索引 (Index),否則分頁查詢在資料量大時會變得很慢。
-
開發者心得:雖然無限滾動看起來很酷,但它會讓使用者找不到「頁尾 (Footer)」,且在回退網頁時容易丟失位置。如果你的部落格文章不多,簡單的 URL 分頁(?page=2)其實是維護成本最低、SEO 最好的選擇。另外,實作分頁時記得要把
totalCount一併回傳,這樣前端才能計算出總共有幾頁。
參考來源:
- Prisma Documentation - Pagination (https://www.prisma.io/docs/orm/prisma-client/queries/pagination)
- Next.js - searchParams (https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional)