Frontend System Design/Pagination & Data Loading
Lesson 11 of 17 · Episode 11

Pagination

Loading large datasets in chunks — offset vs. cursor pagination, the trade-offs, and what breaks under dynamic data.

OffsetCursorInfinite scrollTrade-offs
Watch on YouTube ↗

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.

The problem

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.

The core insight
Will the user really look at all 1,000 items? Almost never. So don't fetch all 1,000. Fetch the smallest useful chunk, and fetch the next one only when it's needed.
Two techniques

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:

InteractiveOffset vs. cursor — page by page
🖥
Client
🗄
Server
p1Shipped the new dashboard
p2Why we moved to cursor pagination
p3RADIO in practice
p4Frontend perf notes
p5Live from EgyptJS
p6Refactor diary
p7Edge cases I missed
p8A tiny CSS win
p9Reading the source
p10Weekend project
p11Interview retro
p12Hello, world
0 / 12 loaded

Offset says skip 0, then take 4. Same data for a static list — the difference shows up when the list changes.

Switch the technique and watch how the request the client sends differs, even though the data returned for a static list is identical.

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:

page 2, size 3
SELECT * FROM posts
  ORDER BY id DESC
  LIMIT 3 OFFSET 3;   -- skip the first page, take the next 3

A 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:

next page after post 'p3'
SELECT * FROM posts
  WHERE id < 'p3'      -- everything older than the last item you saw
  ORDER BY id DESC
  LIMIT 3;
Tip
Cursors are usually opaque and hashed before being handed to the client (e.g. c_1a4f9) so you're not exposing raw database ids or letting clients craft arbitrary queries.
Where offset breaks

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:

InteractiveA new post arrives mid-scroll
page size = 3
Server feed (newest first)
APost Apage 1
BPost Bpage 1
CPost Cpage 1
DPost D
EPost E
FPost F
What the user's feed shows
Nothing loaded yet.
Load page 1, let a new post appear at the top, then load page 2. In offset mode an item gets shown twice (and a new one is skipped). Cursor mode is unaffected.

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.

Watch out
If you're stuck with offset on dynamic data, you end up bolting on client-side de-duplication logic — tracking ids you've already seen, dropping repeats, sometimes firing extra requests to backfill. It works, but it's complexity that cursor pagination avoids by design.
The frontend

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.

InteractiveThree ways to surface the same data

Classic offset territory — you can jump straight to any page because you know the total count.

p1Shipped the new dashboard
p2Why we moved to cursor pagination
p3RADIO in practice
p4Frontend perf notes
Numbered pages need a known total and the ability to jump around — that's offset's strength. Load-more and infinite scroll only ever go forward — perfect for cursor.
The hidden cost

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:

InteractiveRows scanned to reach a page
Offset-based20 rows
scans 20 rows, then discards the first 0
Cursor-based20 rows
reads 20 rows — jumps straight to the cursor via the index

Offset cost grows linearly with the page number: to reach page 1 the database walks every row before it. Cursor cost stays flat no matter how deep you go — that's why it wins on large, deep datasets.

Offset cost grows linearly with depth; cursor cost is constant. On a large table the gap becomes enormous.
Side by side

Choosing between them

Offset-basedCursor-based
Jump to a specific pageYes — its signature strengthNo (or only awkwardly)
Knows total page countYesNot naturally
Stable under inserts/deletesNo — duplicates & skipsYes — rock solid
Performance on deep pagesDegrades linearlyConstant
Implementation effortTrivialMore involved (hashing, fallbacks)
Best fitSmall, fairly static data; admin tables; anything needing page numbersLarge or fast-changing data; feeds; infinite scroll
It depends — but now you know on what
There's no universal winner. The choice falls out of your requirements, your data (size + how fast it changes), your use case, and the UX you want. The job is knowing the trade-offs well enough to defend your pick — in an interview, or in a design doc.
Q1Sort each scenario
Match each screen to the technique that fits best — tap Offset or Cursor for each, and get instant feedback.
Twitter / X home feed (new posts constantly)
Admin orders table with “go to page 7”
Google-style search results (1 2 3 … 10)
Instagram Explore — endless image grid
Back-office table of 2M rows, deep pages
Q2Multiple choice
You're building a Twitter-style feed where new posts arrive constantly. Which pagination technique avoids showing duplicates as the user scrolls?
Q3Multiple choice
With page size 10, which offset jumps to page 4?
Q4Multiple choice
Your admin dashboard shows a stable table and users need to jump to specific page numbers. Best fit?
Try it yourself

Design challenge

Before the next lesson, sketch the pagination for these three screens and justify offset vs. cursor for each:

  1. A Google-style search results page with “1 2 3 … 10” pager.
  2. An Instagram-style home feed with stories at the top.
  3. 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.

Key takeaways
  • 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.
← Previous
10. Customizing Component Appearance
Next →
12. Icon Rendering — Part 1