פagination של אתר: offset, cursor, SEO ו-prefetch
הערה: כאשר קטלוג עולה על 10,000 פריטים או טבלת לוגים מכילה 500,000 שורות, pagination הוא חיוני. בפרקטיקה שלנו, היה פרויקט עם 1.5 מיליון רשומות ב-PostgreSQL — COUNT בוצע ב-4 שניות גם לאחר אינדוקס. משתמשים איבדו את המיקום שלהם בלחיצה על 'Back', ובוטים של חיפוש לא אינדקסו את כל הדפים. יישמנו pagination עבור יותר מ-50 פרויקטים, מחנויות e-commerce ועד דשבורדים אנליטיים. זה קריטי במיוחד עבור פרויקטים ב-Laravel ו-Next.js, שבהם pagination שגוי מוביל לפגיעה ב-Core Web Vitals וירידה בהמרות. במאמר זה, נפרק את היישום של pagination: מבחירת אלגוריתם ועד תגי SEO ו-prefetch.
בחירת סוג pagination לחנות e-commerce
הבחירה בין offset ל-cursor תלויה במשימה. Offset pagination הוא פשוט: LIMIT 20 OFFSET 40. אבל על מערכי נתונים גדולים, COUNT הוא צוואר בקבוק. אנו משתמשים בספירה מקורבת באמצעות pg_class.reltuples ב-PostgreSQL, ומאיצים את השאילתה פי 100. Cursor pagination מהיר יותר אך אינו מספק URL לכל דף — בעיה עבור SEO.
| קריטריון | Offset | Cursor |
|---|---|---|
| מהירות על 1M שורות | 2-4 שניות (COUNT) | <10 אלפיות השנייה |
| יציבות תחת הוספות | הזזה | לא |
| תאימות SEO | כן | לא |
| מורכבות יישום | נמוכה | בינונית |
למה prefetch חשוב למכשירים ניידים?
Prefetch מאפשר טעינת הדף הבא לפני לחיצה. במכשירים ניידים עם חיבורים איטיים, זה מפחית את INP ב-40%. אנו משתמשים ב-react-query עם staleTime: 30_000 וטוענים נתונים מראש ב-useEffect. דוגמה עבור Next.js:
useEffect(() => {
if (page < lastPage) {
queryClient.prefetchQuery({
queryKey: ['products', page + 1],
queryFn: () => fetchProducts(page + 1),
staleTime: 30_000,
});
}
}, [page]); השוואת ביצועים: prefetch לעומת ללא prefetch
| תרחיש | LCP | INP | זמן מעבר |
|---|---|---|---|
| ללא prefetch | 2.8 שניות | 200 אלפיות השנייה | 600 אלפיות השנייה |
| עם prefetch | 2.1 שניות | 120 אלפיות השנייה | 100 אלפיות השנייה |
מתי להשתמש ב-cursor pagination?
Cursor pagination מתאים לפידים בזמן אמת (Twitter, פיד תגובות) או ל-APIs עם נפחי נתונים גדולים שבהם סדר ללא הזזות חשוב. אנו משתמשים בו בפרויקטים עם יותר מ-100,000 רשומות עם הוספות תכופות. Cursor pagination דורש מפתח מיון ייחודי (למשל, ID או created_at) ואינו תומך בקפיצות שרירותיות בין דפים, אך מבטיח יציבות כאשר מתווספות רשומות חדשות.
מדריך שלב-אחר-שלב ליישום pagination ב-Next.js
- הגדר את חוזה ה-API: בחר offset או cursor. עבור offset, החזר
useEffect(() => { if (page < lastPage) { queryClient.prefetchQuery({ queryKey: ['products', page + 1], queryFn: () => fetchProducts(page + 1), staleTime: 30_000, }); } }, [page]);,current_page,last_page,total. - יישם לוגיקת שרת: השתמש ב-
per_pageב-Laravel או ב-paginate()ב-MongoDB. שמור COUNT במטמון באמצעות Redis למשך 5 דקות. - פתח את רכיב ה-UI: עם אליפסיס, aria-label, ורספונסיביות.
- סנכרן עם ה-URL: באמצעות
find().skip().limit()ב-Next.js אוuseSearchParamsב-JavaScript ונילה. - הוסף prefetch: עם React Query או
history.pushStateעםuseEffect. - יישם תגי SEO:
fetchו-<link rel="prev"/>עבור Yandex. Google אינו משתמש ב-rel prev/next לדירוג (Google Search Central), אך הם חשובים עבור Yandex. - בדוק: ודא ש-LCP < 2.5 שניות, INP < 200 אלפיות השנייה.
צד שרת: offset לעומת cursor pagination
כדי להאיץ את COUNT, אנו משתמשים בספירה מקורבת באמצעות <link rel="next"/> ב-PostgreSQL או במטמון ב-Redis למשך 5 דקות. זה מפחית את זמן השאילתה משניות לאלפיות השנייה.
// Laravel — стандартный paginate()
public function index(Request $request): JsonResponse
{
$perPage = min($request->integer('per_page', 20), 100);
$products = Product::where('is_active', true)
->orderByDesc('created_at')
->paginate($perPage);
return response()->json([
'data' => ProductResource::collection($products->items()),
'current_page' => $products->currentPage(),
'last_page' => $products->lastPage(),
'per_page' => $products->perPage(),
'total' => $products->total(),
'from' => $products->firstItem(),
'to' => $products->lastItem(),
]);
} רכיב UI ל-pagination
הרכיב מציג מספרי עמודים עם אליפסיס עבור עמודים רבים. לכפתורים יש pg_class.reltuples לנגישות. העמוד הפעיל מודגש. מעוצב עם Tailwind.
// components/Pagination.tsx
interface PaginationProps {
currentPage: number
lastPage: number
onPageChange: (page: number) => void
}
export function Pagination({ currentPage, lastPage, onPageChange }: PaginationProps) {
const pages = buildPageRange(currentPage, lastPage)
return (
<nav aria-label="Навигация по страницам">
<ul className="flex items-center gap-1">
<li>
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Предыдущая страница"
>
←
</button>
</li>
{pages.map((page, i) =>
page === '...' ? (
<li key={`ellipsis-${i}`} aria-hidden="true">…</li>
) : (
<li key={page}>
<button
onClick={() => onPageChange(page as number)}
aria-current={page === currentPage ? 'page' : undefined}
className={page === currentPage ? 'font-bold' : ''}
>
{page}
</button>
</li>
)
)}
<li>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === lastPage}
aria-label="Следующая страница"
>
→
</button>
</li>
</ul>
</nav>
)
}
function buildPageRange(current: number, last: number): (number | '...')[] {
if (last <= 7) return Array.from({ length: last }, (_, i) => i + 1)
const delta = 2
const range: (number | '...')[] = []
const left = current - delta
const right = current + delta
let prev: number | null = null
for (let i = 1; i <= last; i++) {
if (i === 1 || i === last || (i >= left && i <= right)) {
if (prev !== null && i - prev > 1) range.push('...')
range.push(i)
prev = i
}
}
return range
} סנכרון URL
מספר העמוד חייב להיות ב-URL — אחרת המשתמש מאבד את המיקום ברענון ואינו יכול לשתף את הקישור. אנו משתמשים ב-// Laravel — стандартный paginate() public function index(Request $request): JsonResponse { $perPage = min($request->integer('per_page', 20), 100); $products = Product::where('is_active', true) ->orderByDesc('created_at') ->paginate($perPage); return response()->json([ 'data' => ProductResource::collection($products->items()), 'current_page' => $products->currentPage(), 'last_page' => $products->lastPage(), 'per_page' => $products->perPage(), 'total' => $products->total(), 'from' => $products->firstItem(), 'to' => $products->lastItem(), ]); } ב-Next.js App Router.
// Next.js App Router 'use client'
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
function usePageParam() {
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const page = Number(searchParams.get('page') ?? '1')
const setPage = (newPage: number) => {
const params = new URLSearchParams(searchParams.toString())
params.set('page', String(newPage))
router.push(`${pathname}?${params.toString()}`, { scroll: true })
}
return [page, setPage] as const
} מה כלול בעבודת ה-pagination שלנו
- ביקורת על תוכנית ה-pagination הנוכחית (שאילתות SQL, זמן תגובה)
- עיצוב חוזה API (offset/cursor, פורמט תגובה)
- יישום לוגיקת שרת עם מטמון COUNT
- פיתוח רכיב UI עם רספונסיביות ונגישות
- סנכרון URL (Next.js, React Router, Vanilla)
- הגדרת prefetch למעברים מיידיים
- שילוב תגי SEO (rel prev/next, canonical, sitemap עבור pagination)
- תיעוד והדרכה לצוות הלקוח
לוחות זמנים ועלות
רכיב בסיסי עם אינטגרציית API — מיום אחד. חבילה מלאה עם סנכרון URL, prefetch, תגי SEO, עיצוב רספונסיבי והדרכה — מיומיים עד 4 ימים. העלות מחושבת באופן אישי לאחר ניתוח הפרויקט שלך. חיסכון בזמן פיתוח על רכיבים מוכנים הוא עד 30% בהשוואה לכתיבה מאפס.
צור קשר להערכה חינמית של הפרויקט שלך. המהנדסים שלנו עם שנים של ניסיון יעזרו ליישם pagination תוך התחשבות ב-SEO וביצועים. אנו מבטיחים LCP < 2.5 שניות ואינדוקס נכון ב-Yandex. הזמן יישום pagination עם אחריות ל-Core Web Vitals. קבל ייעוץ מהנדס ואב-טיפוס ביום אחד.







