ページネーションは、モバイルアプリやウェブサービスで大量のレコードを扱う際に使用される、データをページ単位で読み込む技法です。Android Developers Documentation (2025)によれば、適切なページネーションの実装はAPIの負荷を減らし、トラフィックを節約し、ユーザー体験を向上させます。ページ単位の読み込みにより、アプリは全データの読み込み完了を待たずに、コンテンツを段階的に表示できます。
重要ポイント
ページネーション(ラテン語のpagination — ページ分割に由来)は、大規模なデータセットを順次的な部分(ページ)に分割する技法です。モバイルアプリでは、メッセージリスト、ニュースフィード、商品カタログ、注文履歴など、潜在的に無制限のレコードを含むコレクションを読み込む際にページネーションが使用されます。
ページネーションがないと、アプリはすべてのデータを一度に読み込む必要があり、長時間の待機、高いトラフィック消費、低性能デバイスでの不安定な動作を引き起こします。ページネーションを用いたAPIリクエストは、データの一部分と次の部分を読み込むためのメタ情報のみを返すため、アプリは受信する情報量を制御できます。
ページネーションの主な指標は、ページサイズ — 1ページあたりのレコード数(通常10〜50)、およびページ番号またはカーソル — データセット内の現在位置を示すポインタです。ページサイズの選択はデータの種類によって異なります。コンパクトな要素(名前)の場合は20〜30で十分ですが、画像付きカードの場合は10〜15です。
モバイルデバイスには、RAM、プロセッサ速度、トラフィック制限といった限られたリソースがあります。ページネーションは3つの主要な課題を解決します。メモリ消費の削減(表示されている要素のみがメモリに保持される)、初回表示の高速化(最初のチャンクが全セットより速く読み込まれる)、トラフィックの節約(ユーザーがリストをスクロールしたときのみデータが読み込まれる)。
ページネーションには4つの主要な種類があり、それぞれ特定の課題を解決します。方式の選択は、データの一貫性要件、APIアーキテクチャ、ストレージの種類、クライアントおよびサーバー側の実装の複雑さによって決まります。
| 種類 | 仕組み | 安定性 | 大規模データでの速度 |
|---|---|---|---|
| Offset | SQLのLIMIT + OFFSET | 低い | OFFSET増大とともに低下 |
| Cursor | WHERE id > last_id | 高い | 安定(O(log n)) |
| Keyset | WHERE key > last_key | 高い | 安定(O(log n)) |
| Time-based | WHERE created_at < last_time | 中程度 | インデックスで安定 |
Offsetページネーションは、シンプルな実装が重要な静的データセットや更新頻度の低いデータセットに適しています。CursorとKeysetは、頻繁な挿入がある動的データに適しています。Time-basedは、レコードが作成時間順に並ぶ時系列フィードに適しています。GraphQL標準のRelayは、唯一の推奨方式としてカーソルページネーションを使用しています。
Offsetページネーションは、ページベースの読み込みの最もシンプルな方式です。クライアントはpageとlimit(またはoffsetとlimit)パラメータを送信し、サーバーはSQLのOFFSETとLIMITを適用します。たとえば、page=2、limit=20は21〜40番目のレコードを返します。この方式は直感的で、任意のスタックで簡単に実装できます。
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
}
Offsetページネーションの主な欠点は、レコードの欠落と重複の問題です。2つのリクエストの間にテーブルに新しいレコードが追加されると、OFFSETがずれてしまい、ユーザーは同じレコードを2回見たり、新しいレコードを見逃したりする可能性があります。これは、一貫性が重要なニュースフィードやチャットで深刻です。
もう1つの問題は、大きなOFFSET値でのパフォーマンス低下です。データベースは結果を返す前に、最初のoffsetレコードをスキャンしてスキップする必要があります。offset=100000でLIMIT 20の場合でも、サーバーはスキャンに顕著な時間を費やします。PostgreSQLとMySQLは、OFFSETの増加に伴って速度が線形に低下します。
Offsetページネーションは以下のケースで最適な選択肢です。管理パネル(データの変更が稀で、ページナビゲーションが必要)、レポートと履歴ログ(データの固定スナップショット)、フィルタ付きカタログ(任意のページにジャンプ可能)。Offsetはクライアント側でも最も実装が簡単で、RecyclerViewとPaging 3で標準サポートされています。
Keysetページネーションは、一意のキー(通常はプライマリキー)を使用してレコードをフィルタリングします。OFFSETの代わりに、WHERE id > last_seen_idというクエリを使用します。これにより、レコード数に関係なく安定したパフォーマンスが保証され、新しいレコードは常に大きなidを持つため、挿入時の重複が発生しません。
-- Offsetページネーション (問題あり)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;
-- Keysetページネーション (安定)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;
Time-basedページネーション(または時間によるカーソル)は、ナビゲーションにcreated_atタイムスタンプを使用します。クライアントは最後に読み込んだレコードのタイムスタンプを送信し、サーバーはそのタイムスタンプより前または後に作成されたレコードを返します。この方式は、レコードの順序が公開時間によって決まるソーシャルネットワークやニュースフィードで人気があります。
Time-basedページネーションの特徴として、2つのレコードが同じミリ秒に作成された場合に重複が発生する可能性があります。この問題を解決するには、時間ベースのキーと一意のidを組み合わせます:WHERE (created_at, id) < (last_time, last_id)。この複合カーソルにより、各レコードの一意性と正確な順序が保証されます。
Keysetページネーションには、一意で単調増加する値(オートインクリメントid、UUID v7)を持つカラムが必要です。Time-basedはcreated_atを持つ任意のテーブルに適していますが、重複処理が別途必要です。主な違い:Keysetはどのような挿入操作でも安定して動作しますが、Time-basedは同一タイムスタンプに影響を受けます。
ページネーション方式の選択は、データの性質とユーザー体験の要件によって異なります。以下に、モバイル開発における典型的なシナリオの推奨事項を示します。万能の解決策はありません — 各方式には最適な適用領域があります。
Android Paging 3ライブラリは、PagingSourceを介してすべてのページネーション方式をサポートしています。Offsetの場合はIntキー(page)を持つPagingSource、Cursorの場合はStringまたはLongキー(cursor)を持つPagingSourceを使用します。PagingSourceは、読み込み、キャッシング、エラー時の再試行を自動的に管理します。
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
}
}
}
ページサイズは読み込み速度とパフォーマンスの体感に影響します。モバイルアプリでは、1ページあたり10〜25アイテムが最適な範囲です。10未満ではAPIへのリクエストが多くなり、スクロールがぎこちなくなります。25を超えると、低速ネットワークでの最初のチャンク読み込みが遅くなります。
画像や動画の場合、各要素にメディアの読み込み時間が追加でかかるため、ページサイズを5〜10に減らします。テキストベースのリスト(コメント、ログ)では、サイズを30〜50レコードまで増やせます。クライアントがさまざまなネットワーク状況に適応できるよう、API経由でページサイズを設定可能にすることを推奨します。
よくある質問
ページネーションとは、データを一度にすべて読み込むのではなく、部分的に読み込むことです。本と同じで、1ページ読んでから次のページに進みます。アプリでは、リストをスクロールすると次のデータチャンクが読み込まれ、リスト全体が読み込まれるわけではないため、トラフィックとメモリを節約できます。
Offsetはレコードを数えます。「20をスキップして、次の10を返す」。読み込みの間に新しいレコードが追加されると、番号がずれます。Cursorは最後のレコードの一意の識別子を使用します。「ID=100の後に10レコードを返す」。新しいレコードは位置に影響しません。
モバイルアプリでは10〜25アイテムが最適です。画像の多いリストでは5〜10、テキストフィードでは20〜30です。サイズは各要素の平均サイズによって異なります。要素が重いほど、高速表示のためにページを小さくする必要があります。
Android JetpackのPaging 3ライブラリを使用します。読み込み用のPagingSource、リアクティブストリーム用のPagingData、スクロール時の自動読み込み用のPagingDataAdapterを提供します。このライブラリは、カスタムPagingSourceを介してOffset、Cursor、Keysetページネーションをサポートしています。
無限スクロールは、リストの終わりに近づくと新しいデータチャンクが自動的に読み込まれるUIパターンです。ページネーションは仕組みであり、データをチャンクで読み込む方法です。無限スクロールはその表示方法の1つです。代替として「もっと読み込む」ボタンがあります。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。