Cursor Pagination dans le développement mobile — définition, principe et réalisation

Auteur : IT Sectr Publié le : 2026-03-11 Temps de lecture : 9 min

Cursor Pagination est une méthode de chargement paginé des données qui utilise un curseur unique pour naviguer dans un ensemble ordonné d’enregistrements. Selon la Spécification GraphQL (2025), la pagination basée sur un curseur est la norme recommandée pour les API travaillant avec des données dynamiques. Cursor pagination élimine les principaux inconvénients de l’approche Offset : instabilité lors des insertions et dégradation des performances sur les grands décalages.

Points clés

  • Cursor Pagination est une méthode de pagination où chaque enregistrement possède un identifiant curseur unique pour la navigation.
  • Curseur est un marqueur de position unique dans un ensemble de données (généralement ID, UUID, timestamp) qui ne change pas lors des insertions.
  • Stabilité — les nouveaux enregistrements ajoutés entre les requêtes ne déplacent pas le curseur, éliminant les doublons et les lacunes.
  • Performance — la requête WHERE id > cursor utilise efficacement l’index sans perdre en vitesse sur les grands ensembles de données.
  • Limitation — la pagination par curseur ne prend pas en charge la navigation par numéro de page (impossible de sauter à la page 5).

Qu’est-ce que Cursor Pagination ?

Cursor Pagination est une méthode de chargement paginé où le serveur renvoie avec les données un pointeur spécial — un curseur. Le client utilise ce curseur dans la requête suivante pour obtenir le lot suivant d’enregistrements. Le curseur est un identifiant unique du dernier élément de la page actuelle.

Contrairement à la pagination Offset, où le client dit « donne-moi la page 5 avec 20 enregistrements », la pagination par curseur fonctionne différemment : « donne-moi 20 enregistrements après l’enregistrement avec ID = 83 ». Le serveur exécute une requête avec WHERE id > 83 et LIMIT 20. Cette approche garantit que chaque enregistrement tombe exactement dans une page indépendamment des insertions.

Le concept de pagination par curseur a été largement adopté grâce à la spécification Relay Connection (GraphQL), qui a fait de la pagination basée sur un curseur la norme pour les API modernes. Relay définit le format de réponse : edges (tableau d’enregistrements avec curseurs), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Historique

La pagination par curseur n’est pas une technique nouvelle — elle était utilisée dans les bases de données bien avant le web. En SQL, on l’appelle keyset pagination ou seek method. La méthode est devenue populaire dans les API après la publication de la spécification Relay en 2015, qui a formalisé le format du curseur comme une chaîne encodée en base64 pour l’uniformité du transport HTTP.

Comment fonctionne la pagination par curseur

Le principe de base de la pagination par curseur est que la requête utilise une condition WHERE sur un champ indexé pour le positionnement, et non un décalage. Pour la direction avant, on utilise WHERE id > last_id ; pour la direction arrière, WHERE id < first_id. L’index B-tree trouve le premier enregistrement après le curseur en O(log n), offrant un temps de réponse stable.

sql
-- Récupérer 20 enregistrements après le curseur '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Récupérer 20 enregistrements AVANT le curseur '83' (retour)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Format du curseur

Un curseur peut être simple (une valeur ID) ou complexe (composé de plusieurs champs). Les curseurs simples sont la clé primaire d’un enregistrement, par exemple, id auto-incrémenté ou UUID. Les curseurs composites sont utilisés pour le tri par champs non uniques, par exemple (created_at, id), où id garantit l’unicité lorsque les horodatages sont identiques.

Un format d’API typique est un curseur sous forme de chaîne encodée en base64. Le serveur décode le curseur, extrait la valeur et construit la requête SQL. L’encodage Base64 cache la structure interne du curseur au client et permet de modifier le format sans rompre la compatibilité ascendante. Le client reçoit les curseurs dans le champ endCursor de la réponse et les transmet comme chaîne dans la requête suivante.

Navigation avant et arrière

La pagination par curseur prend en charge la navigation bidirectionnelle. Pour le déplacement avant (next), on utilise le curseur du dernier élément de la page actuelle ; pour l’arrière (previous), le curseur du premier élément. Les paramètres after et before dans la requête déterminent la direction : after prend les enregistrements après le curseur, before prend les enregistrements avant le curseur.

Cursor vs Offset Pagination

Le choix entre la pagination par curseur et par Offset est l’une des décisions architecturales clés lors de la conception d’une API. Chaque méthode a des forces et des faiblesses qui déterminent son applicabilité. Cursor pagination l’emporte dans les scénarios avec des données dynamiques ; Offset l’emporte dans les scénarios avec navigation arbitraire.

CaractéristiqueCursorOffset
Stabilité lors des insertionsÉlevée (pas de doublons)Faible (décalage de pages)
Performance sur grands ensemblesO(log n) — stableO(n) — se dégrade avec la croissance
Navigation par numéro de pageNonOui (page=5)
Complexité d’implémentationMoyenneFaible
Support RESTcursor/before/afterpage/offset
Support GraphQLNorme RelayNon recommandé

Pourquoi Offset échoue à grande échelle

La pagination Offset effectue un balayage complet de la table jusqu’à la position OFFSET. À offset=100000, la base de données lit et saute 100000 lignes, même si LIMIT est 20. MySQL et PostgreSQL ne peuvent pas optimiser OFFSET — c’est une caractéristique d’implémentation de LIMIT/OFFSET en SQL. La pagination par curseur utilise un index B-tree qui trouve la position en O(log n).

Un problème supplémentaire d’Offset est le « saut » d’enregistrements lors de la pagination arrière. Si un utilisateur a chargé la page 5 et que de nouveaux enregistrements ont été ajoutés à ce moment-là, lors de la demande de la page 6, il verra à nouveau l’enregistrement de la page 5 ou manquera les nouveaux. Cursor pagination élimine complètement ce scénario : le curseur pointe vers un endroit spécifique dans l’ensemble et les insertions ne changent pas la position.

Implémentation de Cursor Pagination

Examinons l’implémentation de la pagination par curseur sur le backend (Kotlin + Spring) et sur le client (Android + Retrofit). Le serveur accepte les paramètres after, before, limit et renvoie une liste d’enregistrements avec curseurs et pageInfo. Une réponse typique contient hasNextPage et hasPreviousPage pour gérer l’interface de pagination.

kotlin
@GetMapping("/posts")
fun getPosts(
    @RequestParam after: Long?,
    @RequestParam(defaultValue = "20") limit: Int
): CursorResponse<Post> {
    val cursor = after ?: Long.MAX_VALUE
    val posts = repository.findByIdLessThanOrderByIdDesc(
        cursor, PageRequest.of(0, limit)
    )
    val endCursor = posts.lastOrNull()?.id
    return CursorResponse(
        data = posts,
        pageInfo = PageInfo(
            hasNextPage = posts.size == limit,
            endCursor = endCursor
        )
    )
}

Implémentation côté client sur Android

Sur le client, la pagination par curseur est implémentée via PagingSource de Paging 3, où la clé est un curseur (Long). PagingSource.load reçoit LoadParams.key — le curseur du dernier enregistrement chargé. LoadResult.Page renvoie les données et nextKey — le curseur pour la page suivante. Quand nextKey = null, la pagination est terminée.

kotlin
// Retrofit API
interface PostApi {
    @GET("posts")
    suspend fun getPosts(
        @Query("after") after: Long?,
        @Query("limit") limit: Int = 20
    ): CursorResponse<Post>
}

// PagingSource with cursor key
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> = try {
        val response = api.getPosts(
            after = params.key,
            limit = params.loadSize
        )
        val nextKey = response.pageInfo.endCursor
        LoadResult.Page(
            data = response.data,
            prevKey = null,
            nextKey = nextKey
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

Implémentation GraphQL via Relay

Dans GraphQL, la pagination par curseur est implémentée via le modèle Relay Connection. Chaque type a une Connection (avec pageInfo et edges) et un Edge (node + cursor). La requête transmet les paramètres first, after, last, before. Le serveur renvoie un tableau d’edges avec curseurs et pageInfo avec hasNextPage/hasPreviousPage.

Quand utiliser Cursor Pagination

La pagination par curseur est recommandée pour les API travaillant avec des données dynamiques où les enregistrements sont fréquemment ajoutés ou supprimés. Exemples classiques : fil d’actualité dans un réseau social, messages de chat, historique des transactions, commentaires de publications. Dans tous ces scénarios, la cohérence et l’absence de doublons sont importantes.

  • Chats et messageries — chaque nouveau message est ajouté en haut de la liste. La pagination Offset est perturbée à chaque nouveau message.
  • Réseaux sociaux et fils — les publications sont publiées en continu. La pagination par curseur garantit que l’utilisateur ne rate aucune publication.
  • Historique des commandes et transactions — les données changent moins fréquemment, mais la cohérence est cruciale pour les rapports financiers.
  • API avec de grands volumes de données — des millions d’enregistrements. Cursor pagination maintient les performances là où Offset commence à ralentir.
  • API GraphQL — la norme Relay exige la pagination basée sur un curseur pour la conformité à la spécification.
  • Applications mobiles avec défilement infini — l’utilisateur défile vers le bas pour charger de nouveaux lots. L’approche par curseur offre une UX fluide sans doublons.

Quand la pagination par curseur n’est pas adaptée

Il existe des scénarios où la pagination Offset est plus pratique : panneaux d’administration où la navigation par numéro de page est nécessaire ; recherche avec pagination où les résultats peuvent changer ; rapports et analyses où un lien fixe vers la page 5 est nécessaire. Dans ces cas, les avantages du curseur ne compensent pas la complexité d’implémentation.

La pagination par curseur ne prend pas en charge le « saut » vers une page arbitraire — l’utilisateur ne peut pas cliquer sur « Page 5 » pour y accéder. C’est une limitation architecturale : compter le nombre total de pages nécessite une requête COUNT séparée, qui peut être coûteuse pour les grandes tables. Dans ces cas, une approche hybride : curseur pour les données + count pour la pagination.

Foire aux questions

Qu’est-ce qu’un curseur dans Cursor Pagination ?

Un curseur est un identifiant unique d’enregistrement qui pointe vers une position dans l’ensemble de données. Il peut être simple (un ID d’enregistrement) ou composite (plusieurs champs). Le client reçoit le curseur du dernier enregistrement de la page et le transmet dans la requête suivante pour obtenir le lot suivant.

Pourquoi Cursor Pagination est-il meilleur qu’Offset ?

Cursor pagination n’est pas sujet au décalage lors de l’ajout de nouveaux enregistrements — chaque élément tombe exactement dans une page. Il maintient également la vitesse sur les grands volumes en utilisant des index au lieu de scanner les premières n lignes. Offset est plus simple mais instable pour les données dynamiques.

Peut-on implémenter la pagination par curseur sans GraphQL ?

Oui, la pagination par curseur n’est pas liée à GraphQL. Elle peut être implémentée dans n’importe quelle API REST en passant le curseur comme paramètre de requête ?after=83&limit=20. La réponse doit contenir pageInfo avec endCursor et hasNextPage — cela permet au client de gérer le chargement sans connaître la structure interne du curseur.

Quel curseur utiliser — ID, UUID ou timestamp ?

ID auto-incrémenté est le choix optimal : il augmente de façon monotone, ne change pas, est indexé efficacement. UUID v7 (ordonné par temps) convient également. Les horodatages peuvent produire des doublons au même moment, donc combinez-le avec ID : (created_at, id) pour garantir l’unicité du curseur.

Comment connaître le nombre total de pages avec la pagination par curseur ?

La pagination par curseur ne fournit pas le nombre total de pages — c’est sa limitation. Si vous avez besoin d’informations totales, exécutez une requête COUNT séparée avec les mêmes filtres. Pour les grandes tables, utilisez un comptage approximatif via EXPLAIN ou un total mis en cache depuis les analyses.

Résumé

  • Cursor Pagination est une méthode de pagination avec navigation par identifiant unique d’enregistrement au lieu d’un décalage.
  • Le curseur garantit la stabilité de l’ensemble lors des insertions : les nouveaux enregistrements ne déplacent pas les pages déjà chargées.
  • Les performances sur les grands volumes de données restent élevées (O(log n)) grâce à l’utilisation de l’index B-tree.
  • Cursor pagination convient aux données dynamiques : chats, fils d’actualité, transactions, commentaires.
  • La limitation principale est l’absence de navigation par numéro de page et l’impossibilité de sauter vers une page arbitraire.
  • L’implémentation utilise WHERE id après le curseur, les paramètres after/before et pageInfo dans la réponse.
  • Norme — Relay Connection GraphQL, mais les API REST avec paramètres de curseur sont également largement utilisées.

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi