Pagination
Loading large datasets in chunks — offset vs. cursor pagination, the trade-offs, and what breaks under dynamic data.
Almost every real UI eventually has to render a list that's bigger than the screen — search results, a feed, a table of users. The naive move is to fetch everything and render it. Pagination is the discipline of refusing to do that: split the data into small chunks and fetch only what the user actually needs, when they need it.
Why pagination exists
Pages usually start fast. The list is small, everything fits, life is good. Then the data grows — 100 items, 1,000, 10,000 — and the same code that fetched “all the posts” quietly becomes the slowest part of the app. You pay for it three times over:
On the network — you transfer a huge payload the user mostly never scrolls to. On the server — it has to read, serialize, and maybe pre-process the whole set. On the client — every item becomes state held in memory and (often) DOM nodes, which is exactly the kind of thing that wrecks frontend performance.
Offset vs. cursor
Underneath every pagination UI there are essentially two strategies for asking the server for “the next chunk.” Offset-based says skip N items, then give me the next M. Cursor-based says start right after the last item I saw, and give me M more. They sound similar; their trade-offs are very different. Play with both:
Offset-based, in SQL
Offset maps almost word-for-word onto a database query, which is exactly why it's so popular — it's trivial to implement:
SELECT * FROM posts
ORDER BY id DESC
LIMIT 3 OFFSET 3; -- skip the first page, take the next 3A common variant is page-based pagination: ?page=2&size=3 instead of ?offset=3&limit=3. It's the same thing wearing a friendlier name — offset = (page - 1) × size.
Cursor-based, in SQL
Instead of counting how many rows to skip, the cursor names the last row you saw and asks for everything after it:
SELECT * FROM posts
WHERE id < 'p3' -- everything older than the last item you saw
ORDER BY id DESC
LIMIT 3;c_1a4f9) so you're not exposing raw database ids or letting clients craft arbitrary queries.The dynamic-data problem
Here's the trap. Offset assumes the list stands still between requests. But real feeds change constantly — someone posts, something gets deleted — and offset counts positions, not items. The instant the list shifts, “skip 3” points at the wrong place. Run the scenario in both modes:
Because cursor pagination anchors to a specific item rather than a position, additions and deletions elsewhere in the list simply don't matter — you always continue from exactly where you were. That stability is its superpower for fast-moving data.
UI patterns
The technique is a backend concern; the shape the user sees is a frontend decision. The same paginated endpoint can power numbered pages, a “load more” button, or an infinite scroll — and the pattern you pick nudges you toward offset or cursor.
Why offset gets slow
Offset has one more sting that only shows up at scale. To return page 500, the database can't teleport to row 10,000 — it has to walk every row before it and throw them away. The deeper the page, the more work wasted. Cursor pagination uses an indexed lookup to jump straight in, so its cost stays flat. Drag the slider:
Choosing between them
| Offset-based | Cursor-based | |
|---|---|---|
| Jump to a specific page | Yes — its signature strength | No (or only awkwardly) |
| Knows total page count | Yes | Not naturally |
| Stable under inserts/deletes | No — duplicates & skips | Yes — rock solid |
| Performance on deep pages | Degrades linearly | Constant |
| Implementation effort | Trivial | More involved (hashing, fallbacks) |
| Best fit | Small, fairly static data; admin tables; anything needing page numbers | Large or fast-changing data; feeds; infinite scroll |
page 4?Design challenge
Before the next lesson, sketch the pagination for these three screens and justify offset vs. cursor for each:
- A Google-style search results page with “1 2 3 … 10” pager.
- An Instagram-style home feed with stories at the top.
- A back-office table of 2 million orders with a “go to page” box.
There are defensible answers for each — the point is the reasoning, not a single “right” technique.
- →Pagination exists to cut network, server, and client cost by fetching small chunks on demand.
- →Offset = skip N, take M. Trivial to build, supports page numbers, but drifts on dynamic data and slows down on deep pages.
- →Cursor = continue after the last item seen. Stable under change and fast at any depth, but can't jump to arbitrary pages.
- →The UI pattern (numbered pages vs. load-more vs. infinite scroll) often decides the technique for you.
- →There's no universal winner — it depends on requirements, data, use case, and UX.