03 thg 10, 2025·8 phút đọc

Cursor vs Offset: phân trang cho API màn hình quản trị nhanh

Tìm hiểu phân trang cursor và offset cùng hợp đồng API nhất quán cho sắp xếp, bộ lọc và tổng số, giúp màn hình quản trị nhanh trên web và di động.

Cursor vs Offset: phân trang cho API màn hình quản trị nhanh

Tại sao phân trang làm giao diện quản trị cảm thấy chậm

Màn hình quản trị thường bắt đầu như một bảng đơn giản: tải 25 hàng đầu, thêm hộp tìm kiếm, xong. Khi có vài trăm bản ghi, mọi thứ có vẻ nhanh. Rồi dữ liệu lớn hơn, và cùng màn hình bắt đầu giật.

Vấn đề thường không phải ở UI. Mà là những gì API phải làm trước khi trả trang 12 với sắp xếp và bộ lọc đã áp dụng. Khi bảng lớn hơn, backend tốn thêm thời gian để tìm các bản ghi phù hợp, đếm chúng và bỏ qua các kết quả trước đó. Nếu mỗi lần nhấp phải kích hoạt truy vấn nặng hơn, màn hình sẽ có cảm giác đang suy nghĩ thay vì phản hồi.

Bạn thường nhận ra điều đó ở cùng những chỗ: chuyển trang chậm dần theo thời gian, sắp xếp trở nên ì ạch, tìm kiếm không nhất quán giữa các trang, và cuộn vô hạn tải ào ạt (nhanh rồi đột ngột chậm). Trong hệ thống bận rộn bạn có thể thấy trùng lặp hoặc mất hàng khi dữ liệu thay đổi giữa các yêu cầu.

UI web và di động cũng kéo phân trang theo hai hướng khác nhau. Bảng quản trị web khuyến khích nhảy đến trang cụ thể và sắp xếp theo nhiều cột. Màn hình di động thường dùng danh sách vô hạn tải khối tiếp theo, và người dùng mong mỗi lần kéo đều nhanh như nhau. Nếu API của bạn chỉ được xây xung quanh số trang, di động thường bị thiệt. Nếu chỉ theo next/after, bảng web có thể cảm thấy hạn chế.

Mục tiêu không chỉ là trả 25 mục. Mà là phân trang nhanh, dự đoán được và ổn định khi dữ liệu tăng, với quy tắc hoạt động giống nhau cho bảng và danh sách vô hạn.

Những khái niệm phân trang cơ bản mà UI phụ thuộc vào

Phân trang là chia một danh sách dài thành các phần nhỏ để màn hình có thể tải và hiển thị nhanh. Thay vì yêu cầu API trả mọi bản ghi, UI yêu cầu lát kết quả tiếp theo.

Điều điều khiển quan trọng nhất là kích thước trang (thường gọi là limit). Trang nhỏ hơn thường cảm thấy nhanh hơn vì server làm ít việc hơn và app vẽ ít hàng hơn. Nhưng trang quá nhỏ lại khiến trải nghiệm giật vì người dùng phải nhấp hoặc cuộn nhiều lần. Với nhiều bảng quản trị, 25–100 mục là phạm vi thực tế, trong đó di động thường thích đầu thấp hơn.

Một thứ tự sắp xếp ổn định quan trọng hơn nhiều nhóm nghĩ. Nếu thứ tự có thể thay đổi giữa các yêu cầu, người dùng sẽ thấy trùng lặp hoặc mất hàng khi phân trang. Sắp xếp ổn định thường nghĩa là sắp xếp theo trường chính (ví dụ created_at) cộng tie-breaker (ví dụ id). Điều này quan trọng cho cả phân trang offset lẫn cursor.

Từ góc nhìn client, phản hồi phân trang nên bao gồm các mục, một gợi ý trang tiếp theo (số trang hoặc token cursor), và chỉ các số liệu UI thực sự cần. Một số màn hình cần tổng chính xác cho “1-50 của 12.340”. Những màn khác chỉ cần has_more.

Phân trang offset: cách hoạt động và điểm đau

Offset pagination là cách trang cổ điển theo trang N. Client yêu cầu một số hàng cố định và nói server bỏ qua bao nhiêu hàng trước. Bạn sẽ thấy nó dưới dạng limitoffset, hoặc pagepageSize mà server chuyển sang offset.

Một yêu cầu điển hình trông như sau:

  • GET /tickets?limit=50\u0026offset=950
  • “Cho tôi 50 ticket, bỏ qua 950 hàng đầu.”

Nó khớp với nhu cầu quản trị thông thường: nhảy đến trang 20, quét bản ghi cũ hơn, hoặc xuất danh sách lớn theo từng phần. Nó cũng dễ giải thích nội bộ: “Xem trang 3 bạn sẽ thấy nó.”

Vấn đề xuất hiện ở các trang sâu. Nhiều cơ sở dữ liệu vẫn phải đi qua các hàng bị bỏ qua trước khi trả trang của bạn, đặc biệt khi thứ tự sắp xếp không được hỗ trợ bởi chỉ mục chặt. Trang 1 có thể nhanh, nhưng trang 200 có thể chậm rõ ràng, và đó chính là điều làm màn hình quản trị cảm thấy ì ạch khi người dùng cuộn hoặc nhảy quanh.

Vấn đề khác là tính nhất quán khi dữ liệu thay đổi. Hãy tưởng tượng quản lý hỗ trợ mở trang 5 của các ticket sắp xếp theo mới nhất. Trong khi họ xem, ticket mới tới hoặc ticket cũ bị xóa. Chèn mới có thể đẩy các mục sang trước (gặp trùng lặp giữa các trang). Xóa có thể đẩy mục lùi (bản ghi biến mất khỏi đường dẫn duyệt của người dùng).

Offset vẫn ổn cho bảng nhỏ, dữ liệu ổn định hoặc xuất một lần. Nhưng trên các bảng lớn, hoạt động cao, các trường hợp biên lộ nhanh.

Phân trang cursor: cách hoạt động và lý do nó ổn định

Cursor pagination dùng một cursor như dấu trang. Thay vì nói “cho tôi trang 7,” client nói “tiếp tục sau mục này”. Cursor thường mã hóa các giá trị sắp xếp của mục cuối (ví dụ created_atid) để server có thể tiếp tục từ đúng chỗ.

Yêu cầu thường chỉ gồm:

  • limit: bao nhiêu mục trả về
  • cursor: token mờ (opaque) từ phản hồi trước (thường gọi là after)

Phản hồi trả các mục cộng một cursor mới trỏ tới cuối lát đó. Sự khác biệt thực tế là cursors không yêu cầu database đếm và bỏ qua hàng. Chúng yêu cầu database bắt đầu từ vị trí đã biết.

Đó là lý do phân trang cursor giữ tốc độ cho danh sách cuộn tiến. Với chỉ mục tốt, database có thể nhảy tới “các mục sau X” rồi đọc limit hàng tiếp theo. Với offset, server thường phải quét (hoặc ít nhất bỏ qua) ngày càng nhiều hàng khi offset tăng.

Về hành vi UI, cursor làm “Next” trở nên tự nhiên: lấy cursor trả về và gửi lại trong yêu cầu kế tiếp. “Previous” là tùy chọn và phức tạp hơn. Một số API hỗ trợ cursor before, trong khi những API khác lấy dữ liệu ngược lại rồi đảo kết quả.

Khi nào chọn cursor, offset hoặc lai

Build faster list endpoints
Xây dựng API danh sách nhanh với sắp xếp ổn định và phân trang bằng cursor, không cần viết tay các endpoint.
Dùng thử AppMaster

Lựa chọn bắt đầu từ cách người dùng thực sự dùng danh sách.

Phân trang cursor phù hợp nhất khi người dùng chủ yếu tiến về phía trước và tốc độ là quan trọng: nhật ký hoạt động, chat, đơn hàng, ticket, audit trail, và phần lớn cuộn vô hạn trên di động. Nó cũng xử lý tốt hơn khi hàng được chèn hoặc xóa trong khi ai đó đang duyệt.

Offset phù hợp khi người dùng thường xuyên nhảy quanh: bảng quản trị truyền thống với số trang, đi tới trang cụ thể, và chuyển nhanh qua lại. Nó dễ giải thích, nhưng có thể chậm trên bảng lớn và kém ổn định khi dữ liệu thay đổi bên dưới bạn.

Cách thực tế để quyết định:

  • Chọn cursor khi hành động chính là “next, next, next.”
  • Chọn offset khi “đi đến trang N” là yêu cầu thực sự.
  • Xem tổng số như tùy chọn. Tổng chính xác có thể tốn kém trên bảng khổng lồ.

Các giải pháp lai phổ biến. Một cách là dùng cursor cho next/prev nhanh, cộng chế độ nhảy trang tùy chọn cho các tập con nhỏ, lọc kỹ nơi offset vẫn nhanh. Một cách khác là truy xuất bằng cursor nhưng hiển thị số trang dựa trên snapshot cache, để bảng cảm giác quen mà không biến mọi yêu cầu thành truy vấn nặng.

Một hợp đồng API nhất quán hoạt động cho web và di động

Giao diện quản trị có cảm giác nhanh khi mọi endpoint danh sách hành xử giống nhau. UI có thể thay đổi (bảng web có số trang, danh sách vô hạn trên di động), nhưng hợp đồng API nên ổn định để bạn không phải học lại quy tắc phân trang cho từng màn hình.

Một hợp đồng thực tế có ba phần: rows, trạng thái phân trang và tổng số tùy chọn. Giữ tên trường giống nhau giữa các endpoint (tickets, users, orders), ngay cả khi chế độ phân trang khác nhau.

Đây là dạng phản hồi phù hợp cho cả web và di động:

{
  "data": [ { "id": "...", "createdAt": "..." } ],
  "page": {
    "mode": "cursor",
    "limit": 50,
    "nextCursor": "...",
    "prevCursor": null,
    "hasNext": true,
    "hasPrev": false
  },
  "totals": {
    "count": 12345,
    "filteredCount": 120
  }
}

Một vài chi tiết giúp dễ tái sử dụng:

  • page.mode cho biết server đang làm gì mà không đổi tên trường.
  • limit luôn là kích thước trang được yêu cầu.
  • nextCursorprevCursor xuất hiện ngay cả khi một trong hai là null.
  • totals là tùy chọn. Nếu tốn kém, chỉ trả khi client yêu cầu.

Bảng web vẫn có thể hiển thị “Page 3” bằng cách giữ chỉ số trang riêng và gọi API nhiều lần. Ứng dụng di động có thể bỏ qua số trang và chỉ yêu cầu lát tiếp theo.

Nếu bạn xây cả web và di động trong AppMaster, một hợp đồng ổn định như thế này mang lại lợi ích nhanh. Cùng hành vi danh sách có thể tái sử dụng giữa các màn hình mà không cần logic phân trang riêng cho từng endpoint.

Quy tắc sắp xếp để giữ phân trang ổn định

Make your API contract consistent
Chuẩn hóa cấu trúc phản hồi một lần và tái sử dụng trên mọi endpoint danh sách.
Get Started

Sắp xếp là nơi phân trang thường vỡ. Nếu thứ tự có thể thay đổi giữa các yêu cầu, người dùng sẽ thấy trùng lặp, khoảng trống, hoặc “mất” hàng.

Hãy biến sắp xếp thành hợp đồng, không phải gợi ý. Công bố các trường và hướng sắp xếp được phép, và từ chối những thứ khác. Điều đó giữ API của bạn dự đoán được và ngăn client yêu cầu các thứ tự chậm trông vô hại trong môi trường phát triển.

Một sắp xếp ổn định cần tie-breaker duy nhất. Nếu bạn sắp xếp theo created_at và hai bản ghi cùng timestamp, thêm id (hoặc cột duy nhất khác) làm khóa cuối cùng. Không có nó, database có thể trả các giá trị bằng nhau theo bất kỳ thứ tự nào.

Các quy tắc thực tế bền vững:

  • Cho phép sắp xếp chỉ trên các trường có chỉ mục, định nghĩa rõ (ví dụ created_at, updated_at, status, priority).
  • Luôn thêm tie-breaker duy nhất cuối cùng (ví dụ id ASC).
  • Định nghĩa một sắp xếp mặc định (ví dụ created_at DESC, id DESC) và giữ nhất quán giữa các client.
  • Ghi chú cách xử lý null (ví dụ “nulls last” cho ngày và số).

Sắp xếp cũng quyết định cách tạo cursor. Cursor nên mã hóa các giá trị sắp xếp của mục cuối theo thứ tự, bao gồm tie-breaker, để trang tiếp theo có thể truy vấn “sau” bộ giá trị đó. Nếu sắp xếp thay đổi, cursor cũ sẽ không hợp lệ. Hãy coi tham số sắp xếp là một phần của hợp đồng cursor.

Bộ lọc và tổng số mà không phá hợp đồng

Bộ lọc nên tách biệt với phân trang. UI đang nói “hiển thị tập hàng khác”, rồi mới hỏi “phân trang tập đó”. Nếu bạn trộn các trường lọc vào token phân trang hoặc coi bộ lọc là tùy chọn và không xác thực, bạn sẽ gặp hành vi khó gỡ lỗi: trang trống, trùng lặp, hoặc cursor đột nhiên trỏ vào tập dữ liệu khác.

Một quy tắc đơn giản: bộ lọc nằm ở tham số truy vấn thông thường (hoặc trong body cho POST), và cursor là mờ và chỉ hợp lệ cho đúng tổ hợp bộ lọc + sắp xếp đó. Nếu người dùng thay đổi bất kỳ bộ lọc nào (status, khoảng ngày, assignee), client nên bỏ cursor cũ và bắt đầu từ đầu.

Hãy nghiêm ngặt về bộ lọc được phép. Điều đó bảo vệ hiệu năng và giữ hành vi dễ dự đoán:

  • Từ chối các trường lọc không biết (đừng im lặng bỏ qua chúng).
  • Xác thực kiểu và phạm vi (ngày, enum, ID).
  • Giới hạn các bộ lọc rộng (ví dụ tối đa 50 ID trong một danh sách IN).
  • Áp cùng bộ lọc cho dữ liệu và totals (không số lệch nhau).

Totals là nơi nhiều API trở nên chậm. Đếm chính xác có thể tốn kém trên bảng lớn, đặc biệt với nhiều bộ lọc. Bạn thường có ba lựa chọn: chính xác, ước lượng, hoặc không có. Chính xác tốt cho dataset nhỏ hoặc khi người dùng thực sự cần “hiển thị 1-25 của 12.431”. Ước lượng thường đủ cho màn hình quản trị. Không có total cũng ổn khi bạn chỉ cần “Tải thêm”.

Để tránh làm chậm mọi yêu cầu, hãy làm totals tùy chọn: chỉ tính khi client yêu cầu (ví dụ cờ includeTotal=true), cache ngắn theo mỗi bộ lọc, hoặc trả totals chỉ ở trang đầu.

Từng bước: thiết kế và triển khai endpoint

Connect API to a real UI
Biến quy tắc phân trang thành UI web hoạt động với bộ xây dựng UI của AppMaster.
Build Web App

Bắt đầu với mặc định. Một endpoint danh sách cần thứ tự sắp xếp ổn định, cộng tie-breaker cho các hàng cùng giá trị. Ví dụ: createdAt DESC, id DESC. Tie-breaker (id) ngăn trùng lặp và khoảng trống khi bản ghi mới được thêm.

Định nghĩa một dạng yêu cầu và giữ nó đơn giản. Tham số điển hình là limit, cursor (hoặc offset), sort, và filters. Nếu bạn hỗ trợ cả hai chế độ, làm chúng loại trừ nhau: hoặc client gửi cursor, hoặc gửi offset, không được cả hai.

Giữ cấu trúc phản hồi nhất quán để web và di động có thể chia sẻ cùng logic:

  • items: trang các bản ghi
  • nextCursor: cursor để lấy trang tiếp (hoặc null)
  • hasMore: boolean để UI quyết định có hiển thị "Tải thêm" không
  • total: tổng bản ghi khớp (null trừ khi được yêu cầu nếu đếm tốn kém)

Triển khai là nơi hai cách tiếp cận khác nhau.

Truy vấn offset thường là ORDER BY ... LIMIT ... OFFSET ..., có thể chậm trên bảng lớn.

Truy vấn cursor dùng điều kiện seek dựa trên mục cuối: “cho tôi các mục nơi (createdAt, id) nhỏ hơn (createdAt, id) cuối”. Điều này giữ hiệu năng ổn định hơn vì database có thể dùng chỉ mục.

Trước khi phát hành, thêm các bảo vệ:

  • Giới hạn limit (ví dụ max 100) và đặt mặc định.
  • Xác thực sort theo allowlist.
  • Xác thực bộ lọc theo kiểu và từ chối key không rõ.
  • Làm cursor mờ (mã hóa các giá trị sắp xếp) và từ chối cursor hỏng.
  • Quyết định cách yêu cầu total.

Kiểm thử với dữ liệu thay đổi dưới bạn. Tạo và xóa bản ghi giữa các yêu cầu, cập nhật trường ảnh hưởng sắp xếp, và kiểm tra bạn không thấy trùng lặp hay mất hàng.

Ví dụ: danh sách tickets nhanh cho web và di động

One API for web and mobile
Sử dụng một hợp đồng phân trang cho cả web và ứng dụng di động native.
Tạo App

Đội hỗ trợ mở màn hình quản trị để xem ticket mới nhất. Họ cần danh sách cảm giác tức thì, ngay cả khi ticket mới tới và agent cập nhật ticket cũ.

Trên web, UI là một bảng. Sắp xếp mặc định theo updated_at (mới nhất trước), và đội thường lọc theo Open hoặc Pending. Cùng endpoint có thể hỗ trợ cả hai hành vi với sắp xếp ổn định và token cursor.

GET /tickets?status=open\u0026sort=-updated_at\u0026limit=50\u0026cursor=eyJ1cGRhdGVkX2F0IjoiMjAyNi0wMS0yNVQxMTo0NTo0MloiLCJpZCI6IjE2OTMifQ==

Phản hồi giữ dự đoán cho UI:

{
  "items": [{"id": 1693, "subject": "Login issue", "status": "open", "updated_at": "2026-01-25T11:45:42Z"}],
  "page": {"next_cursor": "...", "has_more": true},
  "meta": {"total": 128}
}

Trên di động, cùng endpoint cấp năng lượng cho cuộn vô hạn. App tải 20 ticket một lần, rồi gửi next_cursor để lấy lô tiếp theo. Không logic số-trang, và ít bất ngờ hơn khi bản ghi thay đổi.

Chìa khóa là cursor mã hóa vị trí đã thấy cuối cùng (ví dụ updated_at cộng id làm tie-breaker). Nếu một ticket được cập nhật khi agent đang cuộn, nó có thể di chuyển về đầu khi làm mới, nhưng sẽ không gây trùng lặp hay khoảng trống trong feed đã cuộn.

Totals hữu ích nhưng tốn kém trên dataset lớn. Quy tắc đơn giản là trả meta.total chỉ khi người dùng áp bộ lọc (như status=open) hoặc rõ ràng yêu cầu nó.

Sai lầm phổ biến gây trùng lặp, khoảng trống và chậm

Hầu hết lỗi phân trang không phải ở database. Chúng đến từ các quyết định API nhỏ nhìn thì ổn trong test, nhưng vỡ khi dữ liệu thay đổi giữa các yêu cầu.

Nguyên nhân phổ biến nhất của trùng lặp (hoặc mất hàng) là sắp xếp trên trường không duy nhất. Nếu bạn sắp xếp theo created_at và hai mục cùng timestamp, thứ tự có thể đảo giữa các lần yêu cầu. Cách sửa đơn giản: luôn thêm tie-breaker ổn định, thường là khóa chính, và coi sắp xếp như cặp (created_at desc, id desc).

Vấn đề khác là để client yêu cầu bất kỳ kích thước trang nào. Một request lớn có thể làm CPU, bộ nhớ và thời gian phản hồi tăng vọt, làm chậm mọi màn hình quản trị. Chọn mặc định hợp lý và giới hạn cứng, và trả lỗi khi client yêu cầu vượt quá.

Totals cũng có thể gây hại. Đếm tất cả các hàng khớp ở mỗi yêu cầu có thể là phần chậm nhất của endpoint, đặc biệt với bộ lọc. Nếu UI cần totals, chỉ lấy khi được hỏi (hoặc trả ước lượng), và tránh chặn cuộn danh sách vì việc đếm đầy đủ.

Các sai lầm thường gây khoảng trống, trùng lặp và chậm:

  • Sắp xếp không có tie-breaker duy nhất (thứ tự không ổn định)
  • Kích thước trang không giới hạn (quá tải server)
  • Trả totals ở mọi lần (truy vấn chậm)
  • Trộn offset và cursor trong một endpoint (hành vi client khó hiểu)
  • Tái sử dụng cursor khi bộ lọc hoặc sắp xếp thay đổi (kết quả sai)

Reset phân trang mỗi khi bộ lọc hoặc sắp xếp thay đổi. Xem một bộ lọc mới như một tìm kiếm mới: xóa cursor/offset và bắt đầu từ trang đầu.

Checklist nhanh trước khi phát hành

Fix pagination edge cases
Thiết kế bộ lọc, quy tắc sắp xếp và các bảo vệ để UI không còn thấy trùng lặp.
Open Builder

Chạy mục này một lần với API và UI song song. Hầu hết vấn đề xuất phát từ hợp đồng giữa màn hình danh sách và server.

  • Sắp xếp mặc định ổn định và có tie-breaker duy nhất (ví dụ created_at DESC, id DESC).
  • Các trường và hướng sắp xếp được whitelist.
  • Giới hạn kích thước trang, với mặc định hợp lý.
  • Token cursor mờ, và cursor không hợp lệ fail theo cách dự đoán.
  • Mọi thay đổi bộ lọc hoặc sắp xếp reset trạng thái phân trang.
  • Hành vi totals rõ ràng: chính xác, ước lượng, hoặc bỏ qua.
  • Cùng hợp đồng hỗ trợ cả bảng và cuộn vô hạn mà không cần trường hợp đặc biệt.

Bước tiếp theo: chuẩn hóa các danh sách và giữ chúng nhất quán

Chọn một danh sách quản trị mọi người dùng hàng ngày và biến nó thành tiêu chuẩn vàng. Một bảng bận như Tickets, Orders hoặc Users là điểm bắt đầu tốt. Khi endpoint đó nhanh và ổn định, sao chép cùng hợp đồng cho các màn hình quản trị còn lại.

Ghi lại hợp đồng, dù ngắn. Rõ ràng về những gì API chấp nhận và trả để đội UI không đoán và vô tình tạo quy tắc khác nhau cho từng endpoint.

Một tiêu chuẩn đơn giản áp dụng cho mọi endpoint danh sách:

  • Các sắp xếp được phép: tên trường chính xác, hướng, và mặc định rõ (cộng tie-breaker như id).
  • Các bộ lọc được phép: trường nào được lọc, định dạng giá trị, và xử lý khi bộ lọc không hợp lệ.
  • Hành vi totals: khi trả số, khi trả “không biết”, và khi bỏ qua.
  • Hình dạng phản hồi: khóa nhất quán (items, thông tin phân trang, sắp xếp/bộ lọc đã áp, totals).
  • Quy tắc lỗi: mã trạng thái và thông điệp xác thực dễ đọc.

Nếu bạn xây màn hình quản trị bằng AppMaster (appmaster.io), chuẩn hóa hợp đồng phân trang sớm sẽ giúp. Bạn có thể tái sử dụng cùng hành vi danh sách cho web và di động native, và tốn ít thời gian hơn để đuổi các trường hợp biên phân trang sau này.

Câu hỏi thường gặp

What’s the real difference between offset and cursor pagination?

Offset pagination dùng limit cộng offset (hoặc page/pageSize) để bỏ qua các hàng, nên các trang sâu thường chậm hơn vì cơ sở dữ liệu phải đi qua nhiều bản ghi. Phân trang bằng cursor dùng token after dựa trên giá trị sắp xếp của phần tử cuối cùng, nên nó có thể nhảy đến vị trí biết trước và giữ tốc độ khi bạn di chuyển tiếp.

Why does my admin table feel slower the more pages I go through?

Bởi vì trang 1 thường rẻ, nhưng trang 200 bắt buộc cơ sở dữ liệu phải bỏ qua rất nhiều hàng trước khi trả về kết quả. Nếu bạn còn sắp xếp và lọc, khối lượng công việc tăng lên, nên mỗi lần nhấp sẽ giống như một truy vấn nặng thay vì một lần lấy nhanh.

How do I prevent duplicates or missing rows when users paginate?

Luôn dùng một sắp xếp ổn định với tie-breaker duy nhất, ví dụ created_at DESC, id DESC hoặc updated_at DESC, id DESC. Nếu không có tie-breaker, các bản ghi cùng timestamp có thể hoán đổi thứ tự giữa các lần yêu cầu, gây trùng lặp hoặc "mất" hàng.

When should I prefer cursor pagination?

Dùng phân trang cursor cho những danh sách mà người dùng chủ yếu đi tới phía trước và tốc độ là quan trọng, như nhật ký hoạt động, tickets, đơn hàng và cuộn vô hạn trên di động. Cursor giữ nhất quán khi hàng mới được chèn hoặc xóa vì token neo vị trí cuối cùng đã xem.

When does offset pagination still make sense?

Offset phù hợp khi UI thực sự cần "đi đến trang N" và người dùng hay nhảy giữa các trang. Nó cũng tiện cho bảng nhỏ hoặc tập dữ liệu ổn định, nơi việc chậm ở trang sâu và dịch chuyển kết quả ít xảy ra.

What should a consistent pagination API response include?

Giữ một cấu trúc phản hồi thống nhất giữa các endpoint và bao gồm items, trạng thái phân trang và tổng số tùy chọn. Mặc định thực tế là trả về items, một đối tượng page (với limit, nextCursor/prevCursor hoặc offset) và flag nhẹ như hasNext để cả bảng web lẫn danh sách di động đều có thể dùng chung logic client.

Why can totals make pagination slow, and what’s a safer default?

COUNT(*) chính xác trên tập dữ liệu lớn với nhiều bộ lọc có thể là phần chậm nhất của truy vấn và làm mọi chuyển trang cảm thấy lag. Mặc định an toàn là để tổng số tùy chọn: chỉ trả khi được yêu cầu, hoặc trả has_more khi UI chỉ cần "Tải thêm".

What should happen to the cursor when filters or sorting changes?

Xử lý bộ lọc như một phần của tập dữ liệu, và coi cursor chỉ hợp lệ cho chính xác tổ hợp bộ lọc và sắp xếp đó. Nếu người dùng thay đổi bất kỳ bộ lọc hoặc sắp xếp nào, reset phân trang và bắt đầu từ đầu; tái sử dụng cursor cũ khi bộ lọc/sắp xếp đã đổi thường gây ra trang trống hoặc kết quả lẫn lộn.

How do I make sorting fast and predictable for pagination?

Whitelist các trường và hướng sắp xếp được phép, từ chối phần còn lại để client không vô tình yêu cầu thứ tự chậm hoặc không ổn định. Ưu tiên sắp xếp trên các trường có chỉ mục và luôn thêm tie-breaker duy nhất như id để thứ tự xác định được giữa các lần yêu cầu.

What guardrails should I add before shipping a pagination endpoint?

Áp giới hạn tối đa cho limit, kiểm tra kiểu và phạm vi của bộ lọc và sắp xếp, và làm token cursor mờ (opaque) cùng việc xác thực chặt chẽ. Nếu bạn xây màn hình admin bằng AppMaster, giữ những quy tắc này nhất quán trên mọi endpoint danh sách sẽ giúp tái sử dụng hành vi bảng và cuộn vô hạn mà không cần sửa phân trang riêng cho từng màn hình.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu
Cursor vs Offset: phân trang cho API màn hình quản trị nhanh | AppMaster