モバイル開発におけるページネーション — 種類と仕組み

著者: IT Sectr 公開日: 2026-03-11 読了時間: 9 分

ページネーションは、モバイルアプリやウェブサービスで大量のレコードを扱う際に使用される、データをページ単位で読み込む技法です。Android Developers Documentation (2025)によれば、適切なページネーションの実装はAPIの負荷を減らし、トラフィックを節約し、ユーザー体験を向上させます。ページ単位の読み込みにより、アプリは全データの読み込み完了を待たずに、コンテンツを段階的に表示できます。

重要ポイント

  • ページネーション — パフォーマンスとトラフィックを最適化するために、大規模データセットをチャンク単位で読み込む方法。
  • Offsetページネーション はオフセット(page/offset)を使用してナビゲーションする — シンプルだが、頻繁な挿入には不安定。
  • Cursorページネーション は最後のレコードから一意のカーソルを使用する — リクエスト間にデータが変更されても安定。
  • Keysetページネーション は一意のインデックスを持つカラムでフィルタリングする — 重複のない大規模テーブルに効率的。
  • Time-basedページネーション はタイムスタンプでレコードをグループ化する — ニュースフィードやソーシャルネットワークに便利。

ページネーションとは?

ページネーション(ラテン語のpagination — ページ分割に由来)は、大規模なデータセットを順次的な部分(ページ)に分割する技法です。モバイルアプリでは、メッセージリスト、ニュースフィード、商品カタログ、注文履歴など、潜在的に無制限のレコードを含むコレクションを読み込む際にページネーションが使用されます。

ページネーションがないと、アプリはすべてのデータを一度に読み込む必要があり、長時間の待機、高いトラフィック消費、低性能デバイスでの不安定な動作を引き起こします。ページネーションを用いたAPIリクエストは、データの一部分と次の部分を読み込むためのメタ情報のみを返すため、アプリは受信する情報量を制御できます。

ページネーションの主な指標は、ページサイズ — 1ページあたりのレコード数(通常10〜50)、およびページ番号またはカーソル — データセット内の現在位置を示すポインタです。ページサイズの選択はデータの種類によって異なります。コンパクトな要素(名前)の場合は20〜30で十分ですが、画像付きカードの場合は10〜15です。

モバイルアプリにページネーションが必要な理由

モバイルデバイスには、RAM、プロセッサ速度、トラフィック制限といった限られたリソースがあります。ページネーションは3つの主要な課題を解決します。メモリ消費の削減(表示されている要素のみがメモリに保持される)、初回表示の高速化(最初のチャンクが全セットより速く読み込まれる)、トラフィックの節約(ユーザーがリストをスクロールしたときのみデータが読み込まれる)。

ページネーションの主な種類

ページネーションには4つの主要な種類があり、それぞれ特定の課題を解決します。方式の選択は、データの一貫性要件、APIアーキテクチャ、ストレージの種類、クライアントおよびサーバー側の実装の複雑さによって決まります。

種類仕組み安定性大規模データでの速度
OffsetSQLのLIMIT + OFFSET低いOFFSET増大とともに低下
CursorWHERE id > last_id高い安定(O(log n))
KeysetWHERE key > last_key高い安定(O(log n))
Time-basedWHERE created_at < last_time中程度インデックスで安定

各方式を使用するタイミング

Offsetページネーションは、シンプルな実装が重要な静的データセットや更新頻度の低いデータセットに適しています。CursorとKeysetは、頻繁な挿入がある動的データに適しています。Time-basedは、レコードが作成時間順に並ぶ時系列フィードに適しています。GraphQL標準のRelayは、唯一の推奨方式としてカーソルページネーションを使用しています。

Offsetページネーション:利点と欠点

Offsetページネーションは、ページベースの読み込みの最もシンプルな方式です。クライアントはpageとlimit(またはoffsetとlimit)パラメータを送信し、サーバーはSQLのOFFSETとLIMITを適用します。たとえば、page=2、limit=20は21〜40番目のレコードを返します。この方式は直感的で、任意のスタックで簡単に実装できます。

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
    }

データ不整合の問題

Offsetページネーションの主な欠点は、レコードの欠落と重複の問題です。2つのリクエストの間にテーブルに新しいレコードが追加されると、OFFSETがずれてしまい、ユーザーは同じレコードを2回見たり、新しいレコードを見逃したりする可能性があります。これは、一貫性が重要なニュースフィードやチャットで深刻です。

もう1つの問題は、大きなOFFSET値でのパフォーマンス低下です。データベースは結果を返す前に、最初のoffsetレコードをスキャンしてスキップする必要があります。offset=100000でLIMIT 20の場合でも、サーバーはスキャンに顕著な時間を費やします。PostgreSQLとMySQLは、OFFSETの増加に伴って速度が線形に低下します。

Offsetが依然として有効なケース

Offsetページネーションは以下のケースで最適な選択肢です。管理パネル(データの変更が稀で、ページナビゲーションが必要)、レポートと履歴ログ(データの固定スナップショット)、フィルタ付きカタログ(任意のページにジャンプ可能)。Offsetはクライアント側でも最も実装が簡単で、RecyclerViewとPaging 3で標準サポートされています。

KeysetおよびTime-basedページネーション

Keysetページネーションは、一意のキー(通常はプライマリキー)を使用してレコードをフィルタリングします。OFFSETの代わりに、WHERE id > last_seen_idというクエリを使用します。これにより、レコード数に関係なく安定したパフォーマンスが保証され、新しいレコードは常に大きなidを持つため、挿入時の重複が発生しません。

sql
-- 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ページネーション

Time-basedページネーション(または時間によるカーソル)は、ナビゲーションにcreated_atタイムスタンプを使用します。クライアントは最後に読み込んだレコードのタイムスタンプを送信し、サーバーはそのタイムスタンプより前または後に作成されたレコードを返します。この方式は、レコードの順序が公開時間によって決まるソーシャルネットワークやニュースフィードで人気があります。

Time-basedページネーションの特徴として、2つのレコードが同じミリ秒に作成された場合に重複が発生する可能性があります。この問題を解決するには、時間ベースのキーと一意のidを組み合わせます:WHERE (created_at, id) < (last_time, last_id)。この複合カーソルにより、各レコードの一意性と正確な順序が保証されます。

KeysetとTime-basedの比較

Keysetページネーションには、一意で単調増加する値(オートインクリメントid、UUID v7)を持つカラムが必要です。Time-basedはcreated_atを持つ任意のテーブルに適していますが、重複処理が別途必要です。主な違い:Keysetはどのような挿入操作でも安定して動作しますが、Time-basedは同一タイムスタンプに影響を受けます。

プロジェクトに適したページネーション方式の選び方

ページネーション方式の選択は、データの性質とユーザー体験の要件によって異なります。以下に、モバイル開発における典型的なシナリオの推奨事項を示します。万能の解決策はありません — 各方式には最適な適用領域があります。

  • チャット / メッセンジャー — Cursorページネーション(メッセージID順)。新しいメッセージは上部に追加され、カーソルがずれません。
  • ニュースフィード — Time-basedページネーション(created_at順)。レコードは時間順に並び、時系列が重要です。
  • 商品カタログ — Offsetページネーション。ユーザーは特定のページに移動でき、データの変更は稀です。
  • 注文履歴 — Cursorページネーション。読み込みの間に新しい注文が追加されるため、安定性が重要です。
  • コメント — Keysetページネーション。各コメントに一意のidがあり、重複なしで大規模なデータを扱えます。

Paging 3を使用したAndroidでの実装

Android Paging 3ライブラリは、PagingSourceを介してすべてのページネーション方式をサポートしています。Offsetの場合はIntキー(page)を持つPagingSource、Cursorの場合はStringまたはLongキー(cursor)を持つPagingSourceを使用します。PagingSourceは、読み込み、キャッシング、エラー時の再試行を自動的に管理します。

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
        }
    }
}

ページサイズの推奨事項

ページサイズは読み込み速度とパフォーマンスの体感に影響します。モバイルアプリでは、1ページあたり10〜25アイテムが最適な範囲です。10未満ではAPIへのリクエストが多くなり、スクロールがぎこちなくなります。25を超えると、低速ネットワークでの最初のチャンク読み込みが遅くなります。

画像や動画の場合、各要素にメディアの読み込み時間が追加でかかるため、ページサイズを5〜10に減らします。テキストベースのリスト(コメント、ログ)では、サイズを30〜50レコードまで増やせます。クライアントがさまざまなネットワーク状況に適応できるよう、API経由でページサイズを設定可能にすることを推奨します。

よくある質問

ページネーションを簡単に言うと何ですか?

ページネーションとは、データを一度にすべて読み込むのではなく、部分的に読み込むことです。本と同じで、1ページ読んでから次のページに進みます。アプリでは、リストをスクロールすると次のデータチャンクが読み込まれ、リスト全体が読み込まれるわけではないため、トラフィックとメモリを節約できます。

OffsetとCursorページネーションの違いは?

Offsetはレコードを数えます。「20をスキップして、次の10を返す」。読み込みの間に新しいレコードが追加されると、番号がずれます。Cursorは最後のレコードの一意の識別子を使用します。「ID=100の後に10レコードを返す」。新しいレコードは位置に影響しません。

ページネーションの最適なページサイズは?

モバイルアプリでは10〜25アイテムが最適です。画像の多いリストでは5〜10、テキストフィードでは20〜30です。サイズは各要素の平均サイズによって異なります。要素が重いほど、高速表示のためにページを小さくする必要があります。

RecyclerViewでページネーションを実装するには?

Android JetpackのPaging 3ライブラリを使用します。読み込み用のPagingSource、リアクティブストリーム用のPagingData、スクロール時の自動読み込み用のPagingDataAdapterを提供します。このライブラリは、カスタムPagingSourceを介してOffset、Cursor、Keysetページネーションをサポートしています。

無限スクロールとは何ですか?ページネーションとの違いは?

無限スクロールは、リストの終わりに近づくと新しいデータチャンクが自動的に読み込まれるUIパターンです。ページネーションは仕組みであり、データをチャンクで読み込む方法です。無限スクロールはその表示方法の1つです。代替として「もっと読み込む」ボタンがあります。

まとめ

  • ページネーション — あらゆる種類のリストを扱うモバイルアプリに不可欠な、データをチャンクで読み込む技法。
  • Offsetページネーション は実装が簡単だが、挿入時の不整合と大きなOFFSETでのパフォーマンス低下が問題。
  • Cursorページネーション は一意の識別子を使用してナビゲーション — 任意のボリュームで安定かつ効率的。
  • Keysetページネーション はプライマリキーでフィルタリングし、インデックスを活用して最大のパフォーマンスを提供。
  • Time-basedページネーション はタイムスタンプでレコードをグループ化 — 時系列フィードやソーシャルネットワークに最適。
  • 方式の選択 はデータの性質による:動的データにはCursor/Keyset、静的データにはOffset、フィードにはTime-based。
  • AndroidのPaging 3 やiOS/Webの標準カーソルソリューションは、あらゆるページネーション方式に対応したインフラを提供。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください