Phân trang trong phát triển di động — định nghĩa, loại và nguyên lý hoạt động

Tác giả: IT Sectr Đã đăng: 2026-03-11 Thời gian đọc: 9 phút

Phân trang là kỹ thuật tải dữ liệu theo từng trang, được sử dụng trong ứng dụng di động và dịch vụ web để làm việc với tập hợp lớn các bản ghi. Theo Tài liệu Android Developers (2025), việc triển khai phân trang đúng cách giảm tải cho API, tiết kiệm băng thông và cải thiện trải nghiệm người dùng. Tải từng trang cho phép ứng dụng hiển thị nội dung dần dần mà không cần đợi tải toàn bộ dữ liệu.

Những điểm chính

  • Phân trang — phương pháp tải tập dữ liệu lớn theo từng phần để tối ưu hiệu suất và băng thông.
  • Phân trang Offset sử dụng độ lệch (page/offset) để điều hướng — đơn giản nhưng không ổn định khi có nhiều thao tác chèn.
  • Phân trang Cursor sử dụng con trỏ duy nhất từ bản ghi cuối cùng — ổn định khi dữ liệu thay đổi giữa các yêu cầu.
  • Phân trang Keyset lọc theo cột có chỉ mục duy nhất — hiệu quả cho bảng lớn không có trùng lặp.
  • Phân trang dựa trên thời gian nhóm bản ghi theo dấu thời gian — thuận tiện cho bảng tin và mạng xã hội.

Phân trang là gì?

Phân trang (từ tiếng Latin pagination — chia trang) là kỹ thuật chia một tập dữ liệu lớn thành các phần tuần tự (trang). Trong ứng dụng di động, phân trang được sử dụng khi tải danh sách tin nhắn, bảng tin, danh mục sản phẩm, lịch sử đơn hàng và bất kỳ bộ sưu tập nào có số lượng bản ghi tiềm năng không giới hạn.

Không có phân trang, ứng dụng buộc phải tải toàn bộ dữ liệu cùng một lúc, dẫn đến thời gian chờ lâu, tiêu tốn nhiều băng thông và hiệu suất không ổn định trên thiết bị yếu. Yêu cầu API có phân trang chỉ trả về một phần dữ liệu và thông tin meta để tải phần tiếp theo — do đó, ứng dụng kiểm soát được lượng thông tin nhận được.

Các chỉ số chính của phân trang: kích thước trang (page size) — số bản ghi trên một trang (thường 10–50), và số trang hoặc con trỏ — con trỏ đến vị trí hiện tại trong tập dữ liệu. Việc chọn kích thước trang phụ thuộc vào loại dữ liệu: đối với phần tử nhỏ gọn (tên) 20–30 là đủ, đối với thẻ có hình ảnh — 10–15.

Tại sao cần phân trang trong ứng dụng di động

Thiết bị di động có tài nguyên hạn chế: RAM, tốc độ xử lý và giới hạn băng thông. Phân trang giải quyết ba nhiệm vụ chính: giảm tiêu thụ bộ nhớ (chỉ lưu các phần tử hiển thị trong bộ nhớ), tăng tốc hiển thị lần đầu (phần đầu tiên tải nhanh hơn toàn bộ tập dữ liệu) và tiết kiệm băng thông (dữ liệu chỉ được tải khi người dùng cuộn danh sách).

Các loại phân trang chính

Có bốn loại phân trang chính, mỗi loại giải quyết các nhiệm vụ cụ thể. Việc chọn phương pháp phụ thuộc vào yêu cầu về tính nhất quán dữ liệu, kiến trúc API, loại lưu trữ và độ phức tạp triển khai chấp nhận được ở phía máy khách và máy chủ.

LoạiNguyên lý hoạt độngĐộ ổn địnhTốc độ trên khối lượng lớn
OffsetLIMIT + OFFSET trong SQLthấpgiảm khi OFFSET tăng
CursorWHERE id > last_idcaoổn định (O(log n))
KeysetWHERE key > last_keycaoổn định (O(log n))
Time-basedWHERE created_at < last_timetrung bìnhổn định với chỉ mục

Khi nào sử dụng loại nào

Phân trang Offset phù hợp với tập dữ liệu tĩnh hoặc hiếm khi cập nhật, khi sự đơn giản trong triển khai là quan trọng. Cursor và Keyset dành cho dữ liệu động có nhiều thao tác chèn. Phân trang dựa trên thời gian dành cho bảng tin theo trình tự thời gian, nơi các bản ghi được sắp xếp theo thời gian tạo. Tiêu chuẩn GraphQL Relay sử dụng phân trang con trỏ là phương pháp duy nhất được khuyến nghị.

Phân trang Offset: ưu điểm và nhược điểm

Phân trang Offset là loại tải theo trang đơn giản nhất. Máy khách gửi tham số page và limit (hoặc offset và limit), máy chủ áp dụng SQL OFFSET và LIMIT. Ví dụ: page=2, limit=20 trả về các bản ghi từ 21 đến 40. Phương pháp này trực quan và dễ triển khai trên bất kỳ nền tảng nào.

python
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items")
async def get_items(
    page: int = Query(default=1, ge=1),
    limit: int = Query(default=20, le=100)
):
    offset = (page - 1) * limit
    items = await fetch_items(offset, limit)
    total = await count_items()
    return {
        "items": items,
        "total": total,
        "page": page,
        "pages": (total + limit - 1) // limit
    }

Vấn đề không nhất quán dữ liệu

Nhược điểm chính của phân trang Offset là vấn đề bản ghi bị bỏ sót và trùng lặp. Nếu các bản ghi mới được thêm vào bảng giữa hai yêu cầu, OFFSET bị dịch chuyển: người dùng có thể thấy cùng một bản ghi hai lần hoặc bỏ lỡ một bản ghi mới. Điều này rất quan trọng đối với bảng tin và trò chuyện, nơi tính nhất quán là quan trọng.

Một vấn đề khác là suy giảm hiệu suất trên các giá trị OFFSET lớn. Cơ sở dữ liệu phải quét và bỏ qua các bản ghi offset đầu tiên trước khi trả về kết quả. Với offset=100000 ngay cả với LIMIT 20, máy chủ sẽ mất thời gian đáng kể để quét. PostgreSQL và MySQL cho thấy tốc độ giảm tuyến tính khi OFFSET tăng.

Khi nào Offset vẫn tốt

Phân trang Offset vẫn là lựa chọn tốt nhất cho: bảng quản trị (dữ liệu hiếm khi thay đổi, cần điều hướng trang), báo cáo và nhật ký lịch sử (ảnh chụp dữ liệu cố định), danh mục có bộ lọc (có thể chuyển đến bất kỳ trang nào). Offset cũng dễ triển khai nhất ở phía máy khách — RecyclerView với Paging 3 hỗ trợ sẵn.

Phân trang Keyset và dựa trên thời gian

Phân trang Keyset sử dụng khóa duy nhất (thường là khóa chính) để lọc bản ghi. Thay vì OFFSET, truy vấn sử dụng WHERE id > last_seen_id. Điều này đảm bảo hiệu suất ổn định bất kể số lượng bản ghi và không có trùng lặp khi chèn, vì bản ghi mới luôn có id lớn hơn.

sql
-- Phân trang Offset (có vấn đề)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;

-- Phân trang Keyset (ổn định)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;

Phân trang dựa trên thời gian

Phân trang dựa trên thời gian (hoặc con trỏ theo thời gian) sử dụng dấu thời gian created_at để điều hướng. Máy khách gửi dấu thời gian của bản ghi cuối cùng đã tải và máy chủ trả về các bản ghi được tạo trước hoặc sau dấu thời gian đó. Phương pháp này phổ biến trong mạng xã hội và bảng tin nơi thứ tự bản ghi được xác định bởi thời gian đăng.

Một đặc điểm của phân trang dựa trên thời gian là có thể xảy ra trùng lặp nếu hai bản ghi được tạo trong cùng một mili giây. Để loại bỏ vấn đề này, hãy kết hợp khóa dựa trên thời gian với id duy nhất: WHERE (created_at, id) < (last_time, last_id). Con trỏ kết hợp như vậy đảm bảo tính duy nhất của mỗi bản ghi và thứ tự chính xác.

So sánh Keyset và dựa trên thời gian

Phân trang Keyset yêu cầu một cột có giá trị duy nhất và tăng đơn điệu (id tự tăng, UUID v7). Phân trang dựa trên thời gian phù hợp với bất kỳ bảng nào có created_at nhưng cần xử lý trùng lặp bổ sung. Sự khác biệt chính: Keyset hoạt động ổn định trong mọi thao tác chèn, trong khi dựa trên thời gian nhạy cảm với các dấu thời gian giống nhau.

Cách chọn loại phân trang cho dự án của bạn

Việc chọn loại phân trang phụ thuộc vào bản chất của dữ liệu và yêu cầu về trải nghiệm người dùng. Dưới đây là các khuyến nghị cho các tình huống điển hình trong phát triển di động. Không có giải pháp phổ quát — mỗi phương pháp có một lĩnh vực mà nó là tối ưu.

  • Trò chuyện / Nhắn tin — Phân trang Cursor (theo id tin nhắn). Tin nhắn mới xuất hiện ở trên cùng, con trỏ không bị lệch.
  • Bảng tin — Phân trang dựa trên thời gian (theo created_at). Bản ghi được sắp xếp theo thời gian, trình tự thời gian quan trọng.
  • Danh mục sản phẩm — Phân trang Offset. Người dùng có thể chuyển đến trang cụ thể, dữ liệu hiếm khi thay đổi.
  • Lịch sử đơn hàng — Phân trang Cursor. Tính ổn định quan trọng vì đơn hàng mới được thêm vào giữa các lần tải.
  • Bình luận — Phân trang Keyset. Mỗi bình luận có id duy nhất, khối lượng lớn không trùng lặp.

Triển khai trên Android với Paging 3

Thư viện Android Paging 3 hỗ trợ tất cả các loại phân trang thông qua PagingSource. Đối với Offset — PagingSource với khóa Int (page), đối với Cursor — với khóa String hoặc Long (cursor). PagingSource tự động quản lý việc tải, lưu cache và thử lại khi có lỗi.

kotlin
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> {
        val cursor = params.key ?: Long.MAX_VALUE
        return try {
            val response = api.getPosts(cursor, params.loadSize)
            LoadResult.Page(
                data = response.items,
                prevKey = null,
                nextKey = response.items.lastOrNull()?.id
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Long, Post>): Long? {
        return state.anchorPosition?.let {
            state.closestItemToPosition(it)?.id
        }
    }
}

Khuyến nghị về kích thước trang

Kích thước trang ảnh hưởng đến tốc độ tải và cảm nhận về hiệu suất. Đối với ứng dụng di động, phạm vi tối ưu là 10–25 phần tử trên mỗi trang. Ít hơn 10 dẫn đến quá nhiều yêu cầu API và cuộn giật cục. Nhiều hơn 25 dẫn đến tải phần đầu tiên chậm trên mạng chậm.

Đối với hình ảnh và video, giảm kích thước trang xuống 5–10, vì mỗi phần tử cần thêm thời gian để tải phương tiện. Đối với danh sách văn bản (bình luận, nhật ký), kích thước có thể tăng lên 30–50 bản ghi. Nên làm cho kích thước trang có thể cấu hình được qua API để máy khách có thể thích ứng với các điều kiện mạng khác nhau.

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

Phân trang là gì trong các thuật ngữ đơn giản?

Phân trang là tải dữ liệu theo từng phần, không phải tất cả cùng một lúc. Giống như một cuốn sách: bạn đọc một trang, sau đó lật sang trang tiếp theo. Trong ứng dụng, điều này có nghĩa là khi cuộn danh sách, lô dữ liệu tiếp theo được tải, không phải toàn bộ danh sách, giúp tiết kiệm băng thông và bộ nhớ.

Offset khác Cursor như thế nào?

Offset đếm bản ghi: “bỏ qua 20, trả về 10 bản ghi tiếp theo.” Nếu một bản ghi mới được thêm vào giữa các lần tải, số thứ tự bị lệch. Cursor sử dụng định danh duy nhất của bản ghi cuối cùng: “trả về 10 bản ghi sau ID = 100.” Các bản ghi mới không ảnh hưởng đến vị trí.

Kích thước trang tối ưu cho phân trang là gì?

Đối với ứng dụng di động, 10–25 phần tử là tối ưu. Đối với danh sách có hình ảnh — 5–10, đối với bảng tin văn bản — 20–30. Kích thước phụ thuộc vào kích thước trung bình của mỗi phần tử: phần tử càng nặng, trang càng phải nhỏ để hiển thị nhanh.

Làm thế nào để triển khai phân trang trong RecyclerView?

Sử dụng thư viện Paging 3 từ Android Jetpack. Nó cung cấp PagingSource để tải, PagingData cho luồng phản ứng và PagingDataAdapter để tự động tải khi cuộn. Thư viện hỗ trợ phân trang Offset, Cursor và Keyset thông qua PagingSource tùy chỉnh.

Cuộn vô hạn là gì và nó khác phân trang như thế nào?

Cuộn vô hạn là một mẫu giao diện nơi một lô dữ liệu mới tự động tải khi người dùng đến gần cuối danh sách. Phân trang là cơ chế tải dữ liệu theo lô, còn cuộn vô hạn là một cách hiển thị nó. Một giải pháp thay thế là nút “Tải thêm”.

Tổng kết

  • Phân trang — kỹ thuật tải dữ liệu theo phần, cần thiết cho ứng dụng di động với bất kỳ loại danh sách nào.
  • Phân trang Offset đơn giản để triển khai nhưng gặp vấn đề không nhất quán khi chèn và suy giảm hiệu suất trên OFFSET lớn.
  • Phân trang Cursor sử dụng định danh duy nhất để điều hướng — ổn định và hiệu quả trên mọi khối lượng.
  • Phân trang Keyset lọc theo khóa chính, cung cấp hiệu suất tối đa nhờ sử dụng chỉ mục.
  • Phân trang dựa trên thời gian nhóm bản ghi theo dấu thời gian — lý tưởng cho bảng tin theo trình tự thời gian và mạng xã hội.
  • Việc chọn phương pháp phụ thuộc vào bản chất dữ liệu: cho dữ liệu động — Cursor/Keyset, cho tĩnh — Offset, cho bảng tin — Time-based.
  • Paging 3 trên Android và các giải pháp con trỏ tiêu chuẩn trên iOS/web cung cấp cơ sở hạ tầng sẵn sàng cho mọi loại phân trang.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm