Pagination and Infinite Scroll in Next.js: Offset vs Cursor
By Arash Latifi
A practical comparison of offset pagination, cursor pagination, and infinite scroll for App Router, with Server Actions and IntersectionObserver, plus how to choose for a store catalog.
TL;DR — Use cursor pagination for large or ever-changing catalogs, numbered offset pages for shareable and indexable URLs, and infinite scroll only when users actually scroll.
Define the problem first
All three approaches solve the same job: showing more data without reloading the page. They differ in user experience and in cost.
| Approach | Best for | Upside | Cost |
|---|---|---|---|
| offset (numbered pages) | Lists under 500 items | Shareable URLs, bookmarks, solid SEO | Large offsets get slow |
| cursor (keyset) | Big catalogs, live feeds | Constant page speed | No page number in the URL |
| Infinite scroll | Feeds, image-heavy lists | Smooth, zero clicks | Back button breaks, footer gets pushed down |
Rule of thumb: if a user might link to "page 40" or step through search results, use offset. If rows keep being inserted, use cursor.
A clean Server Component data layer
In the App Router every page is a Server Component, so fetch data there. Keep the query in one small function:
The limit + 1 trick is what tells you whether another page exists. Without it you cannot detect the last page, and users scroll through an empty list waiting for nothing.
The bigger mistake to avoid is offset in the database: skip: 50000 makes the engine walk 50,000 records, and you will never notice on a small catalog. Cursor pagination has no such cliff.
Infinite scroll with a Server Action
Keep the data on the server and only grow the list on the client:
The client holds a "Load more" button and appends the result. For product categories this is the simplest and most reliable form of infinite scroll.
IntersectionObserver for real scroll-driven loading
If you genuinely want loading without a click, use IntersectionObserver, not a scroll listener. It is cheaper and fires precisely:
rootMargin: "400px" preloads the next batch before the user reaches the bottom. With 0, every load feels late because the user always waits on the network.
Watch where you place 'use client': this boundary is exactly where people accidentally turn half the app into client code. Keep this component isolated.
What to pick for a store
- Search results and category pages: numbered
offsetpages. The URLs are shareable and indexable. - Homepage and small categories: infinite scroll, but always keep a "Load more" button for keyboard users.
- Orders and notifications:
cursor.
If you ship infinite scroll on a list that only has twelve results, you have only made the UX worse: users see no next-page affordance and assume they have seen everything.
Image-heavy long lists also need image optimization in Next.js. And if the page has filters or sorting, keep filter state in the URL so pagination does not break when a filter changes.
Checklist
- [ ] Stable sort key in the query (e.g.
createdAtplusid) - [ ] No large database offsets
- [ ]
limit + 1used to detect the next page - [ ] Loading state and end-of-list state handled
- [ ] A "Load more" button as the non-scroll fallback
- [ ] A back-to-top control for infinite lists
FAQ
Q: Is offset or cursor faster?
They are not equally fast at depth. Offset degrades as the page number grows because the database walks skipped rows. Cursor stays roughly constant.
Q: Is infinite scroll bad for SEO?
If each item has no addressable URL, crawlers only see the first few items. For important category pages, numbered pagination is safer.
Q: What about filters and search?
Keep filters in the query string and reset the cursor whenever a filter changes. Forget the reset and the list becomes wrong and duplicated.
Sources
- freeCodeCamp: how to implement infinite scroll with Intersection Observer — the
IntersectionObserverpattern used above - r/nextjs thread: infinite scrolling or pagination for SEO — community experience with crawlers and pagination
- Stack Overflow: cursor pagination with Prisma and SWR — wiring a cursor to an ORM and the client
- vercel/next.js discussion: cursor pagination with static generation — the limits cursors hit on static pages
- Offset vs. Cursor Pagination — a deep dive for developers — independent analysis of the tradeoff