Cursor Pagination in mobiele ontwikkeling — wat is het, principe en implementatie

Auteur: IT Sectr Gepubliceerd: 2026-03-11 Leestijd: 9 min

Cursor Pagination — een methode voor het pagineren van gegevens die een unieke cursor gebruikt om door een geordende set records te navigeren. Volgens GraphQL Specification (2025) is cursor-paginering de aanbevolen standaard voor API's die met dynamische gegevens werken. Cursor Pagination elimineert de belangrijkste nadelen van de Offset-benadering: instabiliteit bij het invoegen en prestatieverlies bij grote verschuivingen.

Belangrijkste punten

  • Cursor Pagination — pagineringsmethode waarbij elk record een unieke identificatie-cursor heeft voor navigatie.
  • Cursor — unieke positiemarker in de dataset (meestal ID, UUID, timestamp) die niet verandert bij het invoegen.
  • Stabiliteit — nieuwe records die tussen verzoeken worden toegevoegd, verschuiven de cursor niet, waardoor duplicaten en hiaten worden geëlimineerd.
  • Prestaties — de WHERE id > cursor query gebruikt efficiënt de index, zonder snelheid te verliezen op grote sets.
  • Beperking — cursor-paginering ondersteunt geen navigatie op paginanummer (je kunt niet naar pagina 5 springen).

Wat is Cursor Pagination?

Cursor Pagination (cursor-paginering) — een pagineringsmethode waarbij de server samen met de gegevens een speciale aanwijzer — cursor — retourneert. De client gebruikt deze cursor in het volgende verzoek om het volgende deel van records op te halen. De cursor is de unieke identificatie van het laatste element van de huidige pagina.

In tegenstelling tot Offset-paginering, waar de client zegt „geef me pagina 5 van 20 records”, werkt cursor-paginering anders: „geef me 20 records na het record met ID = 83”. De server voert een query uit met de voorwaarde WHERE id > 83 en LIMIT 20. Deze benadering garandeert dat elk record precies op één pagina terechtkomt, ongeacht het invoegen.

Het concept van cursor-paginering is wijdverspreid geworden dankzij de specificatie Relay Connection (GraphQL), die cursor-based pagination tot standaard heeft gemaakt voor moderne API's. Relay definieert het antwoordformaat: edges (array van records met cursors), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Geschiedenis van ontstaan

Cursor-pagination is geen nieuwe techniek — het werd al lang vóór het verschijnen van het web in databases gebruikt. In SQL heet dit keyset pagination of seek method. De methode werd populair in API's na de publicatie van de Relay-specificatie in 2015, die het cursorformaat formaliseerde als base64-gecodeerde string voor uniformiteit van overdracht via HTTP.

Hoe cursor-paginering werkt

Het basisprincipe van cursor-paginering — de query gebruikt een WHERE-voorwaarde op een geïndexeerd veld voor positionering, niet een verschuiving. Voor voorwaartse richting wordt WHERE id > last_id toegepast, voor achterwaartse richting — WHERE id < first_id. De B-tree index vindt het eerste record na de cursor in O(log n), wat een stabiele responstijd geeft.

sql
-- 20 records ophalen na cursor '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- 20 records ophalen VÓÓR cursor '83' (terug)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Cursorformaat

De cursor kan eenvoudig (ID-waarde) of complex (samengesteld uit meerdere velden) zijn. Eenvoudige cursors zijn de primaire sleutel van het record, bijvoorbeeld auto-increment id of UUID. Complexe cursors worden gebruikt voor sortering op niet-unieke velden, bijvoorbeeld (created_at, id), waarbij id uniciteit garandeert bij identieke tijdstempels.

Typisch API-formaat — cursor als base64-gecodeerde string. De server decodeert de cursor, haalt de waarde eruit en bouwt de SQL-query. Base64-codering verbergt de interne structuur van de cursor voor de client en maakt het mogelijk het formaat te wijzigen zonder achterwaartse compatibiliteit te verliezen. De client ontvangt cursors in het endCursor-veld van het antwoord en geeft ze door als string in het volgende verzoek.

Navigatie vooruit en achteruit

Cursor-paginering ondersteunt bidirectionele navigatie. Voor voorwaartse beweging (next) wordt de cursor van het laatste element van de huidige pagina gebruikt, voor achteruit (previous) — de cursor van het eerste element. De parameters after en before in het verzoek bepalen de richting: after haalt records na de cursor op, before — vóór de cursor.

Cursor vs Offset pagination

De keuze tussen cursor- en Offset-paginering is een van de belangrijkste architecturale vragen bij het ontwerpen van API's. Elke methode heeft sterke en zwakke punten. Cursor-pagination wint in scenario's met dynamische gegevens, Offset — in scenario's met willekeurige navigatie.

KenmerkCursorOffset
Stabiliteit bij invoegenHoog (geen duplicaten)Laag (pagina-verschuiving)
Prestaties op grote setsO(log n) — stabielO(n) — neemt af met groei
Navigatie op paginanummerNeeJa (page=5)
Complexiteit van implementatieGemiddeldLaag
Ondersteuning in RESTcursor/before/afterpage/offset
Ondersteuning in GraphQLRelay-standaardNiet aanbevolen

Waarom Offset verliest op grote schaal

Offset-paginering voert een volledige tabelscan uit tot de OFFSET-positie. Bij offset=100000 leest en overslaat de database 100000 rijen, zelfs als LIMIT 20 is. MySQL en PostgreSQL kunnen OFFSET niet optimaliseren — dit is een kenmerk van de LIMIT/OFFSET-implementatie in SQL. Cursor-pagination gebruikt de B-tree index, die de positie vindt in O(log n).

Een aanvullend probleem van Offset — „overslaan” van records bij achterwaartse paginering. Als de gebruiker pagina 5 heeft geladen en er worden op dat moment nieuwe records toegevoegd, ziet hij bij het opvragen van pagina 6 het record van pagina 5 opnieuw of slaat hij nieuwe over. Cursor-pagination elimineert dit scenario volledig: de cursor wijst naar een specifieke plaats in de set en invoegingen veranderen de positie niet.

Implementatie van Cursor Pagination

Laten we de implementatie van cursor-paginering op de backend (Kotlin + Spring) en op de client (Android + Retrofit) bekijken. De server ontvangt parameters after, before, limit en retourneert een lijst van records met cursors en pageInfo. Een typisch antwoord bevat hasNextPage en hasPreviousPage voor het beheren van de paginerings-UI.

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

Client-implementatie op Android

Aan de clientzijde wordt cursor-paginering geïmplementeerd via PagingSource uit Paging 3, waarbij de sleutel de cursor (Long) is. PagingSource.load ontvangt LoadParams.key — de cursor van het laatst geladen record. LoadResult.Page retourneert gegevens en nextKey — de cursor voor de volgende pagina. Wanneer nextKey = null — is de paginering voltooid.

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

// PagingSource met cursorsleutel
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)
    }
}

GraphQL-implementatie via Relay

In GraphQL wordt cursor-paginering geïmplementeerd via het Connection Relay-patroon. Elk type heeft Connection (met pageInfo en edges) en Edge (node + cursor). Het verzoek geeft parameters first, after, last, before door. De server retourneert een array van edges met cursors en pageInfo met hasNextPage/hasPreviousPage.

Wanneer Cursor Pagination gebruiken

Cursor-pagination wordt aanbevolen voor API's die werken met dynamische gegevens, waar records vaak worden toegevoegd of verwijderd. Klassieke voorbeelden: nieuwsfeed in een sociaal netwerk, chatberichten, transactiegeschiedenis, reacties op berichten. In al deze scenario's zijn consistentie en afwezigheid van duplicaten belangrijk.

  • Chats en messengers — elk nieuw bericht wordt bovenaan de lijst toegevoegd. Offset-paginering raakt bij elk nieuw bericht in de war.
  • Sociale netwerken en feeds — berichten worden continu gepubliceerd. Cursor-paginering garandeert dat de gebruiker geen enkel bericht mist.
  • Bestel- en transactiegeschiedenis — gegevens veranderen minder vaak, maar consistentie is cruciaal voor financiële rapportage.
  • API's met grote datavolumes — miljoenen records. Cursor-pagination behoudt prestaties waar Offset begint te vertragen.
  • GraphQL API — de Relay-standaard vereist het gebruik van cursor-based pagination volgens de specificatie.
  • Mobiele apps met oneindig scrollen — de gebruiker scrollt naar beneden en laadt nieuwe delen. De cursor-benadering biedt een soepele UX zonder duplicaten.

Wanneer Cursor-pagination niet geschikt is

Er zijn scenario's waarin Offset-paginering handiger is: beheerpanelen waar navigatie op paginanummer nodig is; zoeken met paginering waar resultaten kunnen veranderen; rapporten en analyses waar een vaste link naar pagina 5 nodig is. In deze gevallen wegen de voordelen van de cursor niet op tegen de complexiteit van de implementatie.

Cursor-paginering ondersteunt geen „sprong” naar een willekeurige pagina — de gebruiker kan niet op „Pagina 5” klikken en er naartoe gaan. Dit is een architecturale beperking: voor het berekenen van het totale aantal pagina's is een aparte COUNT-query nodig, die duur kan zijn voor grote tabellen. In dergelijke gevallen een hybride benadering: cursor voor gegevens + count voor paginering.

Veelgestelde vragen

Wat is een cursor in Cursor Pagination?

Een cursor is de unieke identificatie van een record die de positie in de dataset aangeeft. Het kan eenvoudig zijn (record-ID) of complex (meerdere velden). De client ontvangt de cursor van het laatste record van de pagina en geeft deze door in het volgende verzoek om het volgende deel te krijgen.

Waarin is Cursor Pagination beter dan Offset?

Cursor-pagination is niet onderhevig aan verschuiving bij het toevoegen van nieuwe records — elk element komt precies op één pagina terecht. Het behoudt ook snelheid op grote volumes door het gebruik van indexen in plaats van het scannen van de eerste n rijen. Offset is eenvoudiger, maar onstabiel voor dynamische gegevens.

Kan cursor-paginering worden geïmplementeerd zonder GraphQL?

Ja, cursor-paginering is niet gebonden aan GraphQL. Het kan worden geïmplementeerd in elke REST API door de cursor als queryparameter door te geven ?after=83&limit=20. Het antwoord moet pageInfo bevatten met endCursor en hasNextPage — dit stelt de client in staat het laden te beheren zonder kennis van de interne cursorstructuur.

Welke cursor gebruiken — ID, UUID of timestamp?

Auto-increment ID — de optimale keuze: stijgt monotoon, verandert niet, wordt efficiënt geïndexeerd. UUID v7 (tijd-geordend) is ook geschikt. Timestamp kan duplicaten geven bij dezelfde tijd, daarom wordt het gecombineerd met ID: (created_at, id) voor garantie van cursoruniciteit.

Hoe kom ik achter het totale aantal pagina's bij cursor-paginering?

Cursor-paginering geeft geen totaal aantal pagina's — dit is de beperking ervan. Als informatie over total nodig is, voer dan een aparte COUNT-query uit met dezelfde filters. Gebruik voor grote tabellen een benaderende telling via EXPLAIN of een gecachte total uit analyses.

Samenvatting

  • Cursor Pagination — pagineringsmethode met navigatie op unieke recordidentificatie in plaats van verschuiving.
  • Cursor garandeert stabiliteit van de set bij het invoegen: nieuwe records verschuiven reeds geladen pagina's niet.
  • Prestaties op grote gegevensvolumes blijven hoog (O(log n)) dankzij het gebruik van B-tree indexen.
  • Cursor-pagination is geschikt voor dynamische gegevens: chats, nieuwsfeeds, transacties, reacties.
  • Belangrijkste beperking — geen navigatie op paginanummer en onmogelijkheid om naar een willekeurige pagina te springen.
  • Implementatie gebruikt WHERE id na cursor, parameters after/before en pageInfo in het antwoord.
  • Standaard — Relay Connection GraphQL, maar REST API met cursorparameters is ook wijdverspreid.

We ontwikkelen een mobiele applicatie turnkey

IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.

Bespreek het project

Lees ook