Leer hoe je snelle dashboardlijsten met 100k rijen bouwt met paginering, virtualisatie, slimme filters en betere queries zodat interne tools soepel blijven werken.

Een lijstscherm voelt meestal prima aan, totdat dat niet meer zo is. Gebruikers merken kleine haperingen: scrollen stottert, de pagina blijft even hangen na een update, filters reageren pas na seconden en je ziet een spinner na elke klik. Soms lijkt het tabblad bevroren omdat de UI-thread druk is.
100k rijen is een veelvoorkomend omslagpunt omdat het elk deel van het systeem tegelijk belast. De dataset is nog normaal voor een database, maar groot genoeg om kleine inefficiënties in de browser en over het netwerk zichtbaar te maken. Als je probeert alles tegelijk te tonen, verandert een eenvoudige pagina in een zware pijplijn.
Het doel is niet om alle rijen te renderen. Het doel is iemand snel te helpen vinden wat nodig is: de juiste 50 rijen, de volgende pagina of een smalle uitsnede op basis van een filter.
Het helpt om het werk in vier delen te splitsen:
Als één onderdeel duur is, voelt het hele scherm traag. Een simpele zoekactie kan een verzoek triggeren dat 100k rijen sorteert, duizenden records terugstuurt en de browser dwingt ze allemaal te renderen. Zo wordt typen traag.
Wanneer teams snel interne tools bouwen (ook met low-code/zuurstof-achtige platforms zoals Koder.ai), zijn lijstschermen vaak de eerste plek waar echte datagroei het verschil toont tussen “werkt op een demo” en “voelt elke dag direct aan”.
Voordat je optimaliseert: beslis wat “snel” voor dit scherm betekent. Veel teams jagen op throughput (alles laden) terwijl gebruikers vooral lage latency nodig hebben (iets snel zien verschijnen). Een lijst kan instant aanvoelen ook al laadt hij nooit alle 100k rijen, zolang hij snel reageert op scrollen, sorteren en filters.
Een praktisch doel is tijd tot de eerste rij, niet tijd tot volledig laden. Gebruikers vertrouwen de pagina wanneer ze de eerste 20–50 rijen snel zien en interacties soepel blijven.
Kies een klein aantal cijfers dat je kunt bijhouden telkens je iets verandert:
COUNT(*) en brede SELECTs)Deze metrics verbinden met veelvoorkomende symptomen. Als de browser-CPU piekt tijdens scrollen, doet de frontend te veel werk per rij. Als de spinner wacht maar scrollen daarna goed is, ligt het meestal aan backend of netwerk. Als het verzoek snel is maar de pagina alsnog bevriest, is het bijna altijd rendering of zwaar client-side werk.
Probeer één experiment: houd de UI hetzelfde, maar laat de backend tijdelijk slechts 20 rijen teruggeven met dezelfde filters. Als het snel wordt, is de bottleneck laadtijd of querytijd. Blijft het traag, kijk dan naar renderen, formattering en per-rij componenten.
Voorbeeld: een interne Orders-pagina voelt traag bij typen in de zoekbalk. Als de API 5.000 rijen terugstuurt en de browser die bij elke toetsaanslag filtert, zal typen haperen. Als de API 2 seconden nodig heeft door een COUNT-query op een niet-geïndexeerd filter, zie je wachten voordat enige rij verandert. Verschillende oorzaken, dezelfde gebruikersklacht.
De browser is vaak de eerste bottleneck. Een lijst kan traag aanvoelen ook als de API snel is, simpelweg omdat de pagina te veel probeert te painten. De eerste regel is eenvoudig: render niet duizenden rijen tegelijk in de DOM.
Zelfs voordat je volledige virtualisatie toevoegt, hou elke rij lichtgewicht. Een rij met geneste wrappers, iconen, tooltips en complexe conditionele stijlen in elke cel kost veel bij elk scrollen en elke update. Geef de voorkeur aan platte tekst, een paar kleine badges en slechts één of twee interactieve elementen per rij.
Een stabiele rijhoogte helpt meer dan het klinkt. Wanneer elke rij dezelfde hoogte heeft, kan de browser layout beter voorspellen en blijft scrollen soepel. Variabele hoogte (omvattende beschrijvingen, uitklapbare notities, grote avatars) zorgt voor extra meten en reflow. Als je extra details nodig hebt, overweeg een zijpaneel of één uitvouwbaar gebied, niet een volledige multi-line rij.
Formattering is een stille kostenpost. Datums, valuta en zware stringbewerking tellen op wanneer ze herhaald worden over veel cellen.
Als een waarde niet zichtbaar is, bereken die dan nog niet. Cache dure formatteringsresultaten en bereken ze on-demand, bijvoorbeeld wanneer een rij zichtbaar wordt of de gebruiker een rij opent.
Een snelle pass die vaak winst oplevert:
Voorbeeld: een interne facturen-tabel die 12 kolommen met valuta en datums formatteert zal stotteren tijdens scrollen. Het cachen van geformatteerde waarden per factuur en het uitstellen van werk voor off-screen rijen kan het instant laten aanvoelen, zelfs vóór diepere backend-optimalisaties.
Virtualisatie betekent dat de tabel alleen de rijen rendert die je daadwerkelijk kunt zien (plus een kleine buffer erboven en eronder). Tijdens scrollen hergebruikt het dezelfde DOM-elementen en verwisselt de data erin. Zo voorkomt het dat de browser tienduizenden rijcomponenten tegelijk probeert te painten.
Virtualisatie past goed bij lange lijsten, brede tabellen of zware rijen (avatars, statuschips, actie-menu’s, tooltips). Het is ook nuttig wanneer gebruikers veel scrollen en een vloeiende, continue weergave verwachten in plaats van pagina-voor-pagina springen.
Het is geen magie. Een paar dingen veroorzaken vaak verrassingen:
De simpelste aanpak is saai maar betrouwbaar: vaste rijhoogte, voorspelbare kolommen en niet te veel interactieve widgets in elke rij.
Je kunt beide combineren: gebruik paginering (of cursor-based load more) om te beperken wat je van de server haalt, en virtualisatie om rendering goedkoop te houden binnen die slice.
Een praktisch patroon is om een normale paginagrootte te fetchen (vaak 100–500 rijen), binnen die pagina te virtualiseren en duidelijke controls te bieden om tussen pagina’s te wisselen. Als je infinite scroll gebruikt, voeg dan een zichtbare “Loaded X of Y” indicator toe zodat gebruikers begrijpen dat ze niet alles zien.
Als je een lijstscherm wilt dat bruikbaar blijft naarmate data groeit, is paginering meestal de veiligste default. Het is voorspelbaar, werkt goed voor admin-workflows (review, edit, approve) en ondersteunt veelvoorkomende behoeften zoals exporteren van “pagina 3 met deze filters” zonder verrassingen. Veel teams keren terug naar paginering nadat ze fancy scroll hebben geprobeerd.
Infinite scroll kan prettig aanvoelen voor casual browsen, maar heeft verborgen kosten. Mensen verliezen vaak het gevoel waar ze zijn, de terugknop keert niet altijd terug naar dezelfde plek en lange sessies kunnen geheugen opbouwen naarmate meer rijen laden. Een middenweg is een "Load more" knop die nog steeds pagina’s gebruikt, zodat gebruikers georiënteerd blijven.
Offset-paginering is de klassieke page=10&size=50-benadering. Het is simpel, maar kan langzamer worden op grote tabellen omdat de database veel rijen moet overslaan om bij latere pagina’s te komen. Het kan ook vreemd aanvoelen wanneer er nieuwe rijen binnenkomen en items tussen pagina’s verschuiven.
Keyset-paginering (vaak cursor-paginering genoemd) vraagt om “de volgende 50 rijen na het laatst geziene item”, meestal met een id of created_at waarde. Het blijft meestal snel omdat het niet veel hoeft te tellen en over te slaan.
Een praktische regel:
Gebruikers vinden totalen fijn, maar een volledige “telt alle overeenkomende rijen” kan duur zijn met zware filters. Opties zijn onder andere counts cachen voor populaire filters, de count op de achtergrond bijwerken nadat de pagina laadt of een geschatte telling tonen (bijv. “10.000+”).
Voorbeeld: een interne Orders-pagina kan resultaten direct tonen met keyset-paginering en daarna de exacte totalen pas vullen wanneer de gebruiker even stopt met filters wijzigen.
Als je dit in Koder.ai bouwt, behandel paginering en count-gedrag vroeg in de schermspecificatie zodat de gegenereerde backend-queries en UI-state elkaar later niet tegenwerken.
De meeste lijstschermen voelen traag omdat ze breed open beginnen: laad alles en vraag de gebruiker het te verfijnen. Draai dat om. Begin met verstandige defaults die een kleine, bruikbare set teruggeven (bijv. Laatste 7 dagen, Mijn items, Status: Open) en maak All time een expliciete keuze.
Tekstzoeken is een andere valkuil. Als je bij elke toetsaanslag een query uitvoert, creëer je een achterstand aan verzoeken en een UI die flikkert. Debounce de zoekinvoer (wacht tot de gebruiker kort stopt met typen) en annuleer oudere verzoeken wanneer een nieuw verzoek begint. Een simpele regel: als de gebruiker nog aan het typen is, raak de server nog niet.
Filteren voelt pas snel als het ook duidelijk is. Toon filterchips boven de tabel zodat gebruikers zien wat actief is en het met één klik kunnen verwijderen. Houd chiplabels menselijk, niet ruwe veldnamen (bijv. Eigenaar: Sam in plaats van owner_id=42). Wanneer iemand zegt “mijn resultaten verdwenen”, is dat meestal een onzichtbaar filter.
Patronen die grote lijsten responsief houden zonder de UI ingewikkeld te maken:
Saved views zijn vaak belachelijk nuttig. In plaats van gebruikers elke keer een perfecte eenmalige filtercombinatie te laten bouwen, geef een paar presets die echte workflows vangen. Een ops-team kan schakelen tussen Failed payments today en High-value customers. Die zijn met één klik begrijpelijk en makkelijker snel backend-vriendelijk te houden.
Als je een interne tool bouwt in een chat-gestuurde builder zoals Koder.ai, behandel filters als onderdeel van de productflow, niet als een bolt-on. Begin bij de meest voorkomende vragen en ontwerp de default view en saved views rond die vragen.
Een lijstscherm heeft zelden dezelfde data nodig als een detailpagina. Als je API alles terugstuurt, betaal je twee keer: de database doet meer werk en de browser ontvangt en rendert meer dan nodig. Query shaping is de gewoonte om alleen te vragen wat de lijst nu nodig heeft.
Begin met alleen de kolommen terug te sturen die je nodig hebt om elke rij te renderen. Voor de meeste dashboards is dat een id, een paar labels, een status, een eigenaar en timestamps. Grote tekst, JSON-blobs en berekende velden kunnen wachten tot de gebruiker een rij opent.
Vermijd zware joins voor de eerste render. Joins zijn prima als ze op indexen slaan en kleine resultaten retourneren, maar ze worden duur als je meerdere tabellen joined en daarna sorteert of filtert op de joined data. Een simpel patroon: haal de lijst snel uit één tabel, laad gerelateerde details on-demand (of batch-load voor alleen zichtbare rijen).
Beperk sorteeropties en sorteer op geïndexeerde kolommen. “Sorteren op alles” klinkt handig, maar dwingt vaak trage sorts op grote datasets af. Geef de voorkeur aan een paar voorspelbare keuzes zoals created_at, updated_at of status en zorg dat die kolommen geïndexeerd zijn.
Wees voorzichtig met server-side aggregatie. COUNT(*) op een grote gefilterde set, DISTINCT op een brede kolom of totale pagina-berekeningen kunnen je responstijd domineren.
Een praktische aanpak:
COUNT en DISTINCT als optioneel en cache of benader waar mogelijkAls je interne tools op Koder.ai bouwt, definieer dan een lichte list-query apart van de detail-query in de planningsfase zodat de UI snel blijft naarmate data groeit.
Als je een lijstscherm wilt dat snel blijft bij 100k rijen, moet de database minder werk per verzoek doen. De meeste trage lijsten zijn geen kwestie van “te veel data”, maar van het verkeerde data-toegangs-patroon.
Begin met indexen die matchen wat je gebruikers echt doen. Als je lijst meestal gefilterd wordt op status en gesorteerd op created_at, wil je een index die beide ondersteunt, in die volgorde. Anders kan de database veel meer rijen scannen en vervolgens sorteren, wat snel duur wordt.
Fixes die vaak de grootste winst geven:
tenant_id, status, created_at).OFFSET-pagina’s. OFFSET laat de database veel rijen overslaan.Een simpel voorbeeld: een Orders-tabel die klantnaam, status, bedrag en datum toont. Join niet elke gerelateerde tabel en haal niet de volledige ordernotities voor de lijstweergave. Geef alleen kolommen terug die in de tabel gebruikt worden en laad de rest in een apart verzoek wanneer de gebruiker op een order klikt.
Als je bouwt met een platform zoals Koder.ai, houd deze denkwijze vast zelfs als de UI gegenereerd wordt vanuit chat. Zorg dat de gegenereerde API-endpoints cursor-paginering en selectieve velden ondersteunen zodat de database-werkbelasting voorspelbaar blijft als de tabel groeit.
Als een lijstpagina vandaag traag voelt, begin dan niet met alles herschrijven. Begrens eerst wat normaal gebruik is en optimaliseer dat pad.
Definieer de default view. Kies default filters, sortering en zichtbare kolommen. Lijsten worden traag als ze standaard proberen alles te tonen.
Kies een paging-stijl die bij je gebruik past. Als gebruikers meestal de eerste paar pagina’s scannen, is klassieke paginering prima. Gaan mensen diep (pagina 200+) of heb je stabiele prestaties nodig ongeacht diepte, gebruik keyset-paginering (op basis van een stabiele sortering zoals created_at plus een id).
Voeg virtualisatie toe voor de tabelbody. Zelfs als de backend snel is, kan de browser chokeën als hij te veel rijen tegelijk moet renderen.
Maak zoeken en filters instant. Debounce typen zodat je niet op elke toetsaanslag een verzoek stuurt. Houd filterstate in de URL of in één gedeelde state-store zodat refresh, back-knop en delen werken. Cache het laatst succesvolle resultaat zodat de tabel niet leeg flikkert.
Meet, tune queries en indexen. Log servertijd, database-tijd, payloadgrootte en rendertijd. Trim vervolgens de query: selecteer alleen de kolommen die je toont, pas filters vroeg toe en voeg indexen toe die passen bij je default filter + sort.
Voorbeeld: een supportdashboard met 100k tickets. Default naar Open, toegewezen aan mijn team, gesorteerd op nieuwste, zes kolommen tonen en alleen ticket id, subject, assignee, status en timestamps ophalen. Met keyset-paginering en virtualisatie houd je zowel database als UI voorspelbaar.
Als je interne tools bouwt in Koder.ai, past dit plan goed bij een iterate-and-check workflow: pas de view aan, test scroll en zoek, en tune de query tot de pagina snappy blijft.
De snelste manier om een lijst kapot te maken is 100k rijen behandelen als een normale pagina. De meeste trage dashboards hebben een paar voorspelbare valkuilen.
Een grote fout is alles renderen en het verbergen met CSS. Ook al lijken er maar 50 rijen zichtbaar, de browser betaalt nog steeds voor het aanmaken van 100k DOM-nodes, ze meten en repaints bij scroll. Render alleen wat de gebruiker kan zien (virtualisatie) en houd rijcomponenten simpel.
Zoeken kan ook stilletjes prestaties ruïneren wanneer elke toetsaanslag een volledige tabelscan triggert. Dat gebeurt wanneer filters niet op indexen steunen, wanneer je zoekt over te veel kolommen of wanneer je contains-queries op enorme tekstvelden uitvoert zonder plan. Een goede regel: het eerste filter waar een gebruiker naar grijpt moet goedkoop zijn in de database, niet alleen handig in de UI.
Een andere veelvoorkomende fout is het fetchen van volledige records terwijl de lijst alleen samenvattingen nodig heeft. Een rij heeft meestal 5–12 velden nodig, niet het hele object, geen lange beschrijvingen en geen gerelateerde data. Extra data ophalen vergroot databasewerk, netwerkvertraging en frontend parsing.
Export en totalen kunnen de UI laten vastlopen als je ze op de main thread berekent of wacht op een zwaar verzoek vóór je reageert. Houd de UI interactief: start exports op de achtergrond, toon voortgang en voorkom het herberekenen van totalen bij elke filterwijziging.
Ten slotte kunnen te veel sorteeropties averechts werken. Als gebruikers op elke kolom kunnen sorteren, eindig je met sorteeracties op grote resultsets in geheugen of dwing je de database in trage plannen. Houd sorts bij een kleine set geïndexeerde kolommen en zorg dat de default sort overeenkomt met een echte index.
Snelle controle:
Behandel lijstprestaties als een productfeature, niet als een eenmalige tweak. Een lijstscherm is snel alleen wanneer het snel aanvoelt terwijl echte mensen scrollen, filteren en sorteren op echte data.
Gebruik deze checklist om te bevestigen dat je de juiste dingen hebt gefixt:
Een simpele reality check: open de lijst, scroll 10 seconden en pas dan een veelgebruikt filter toe (bijv. Status: Open). Als de UI bevriest, is het probleem meestal rendering (te veel DOM-rijen) of zware client-side transformaties (sorteren, groeperen, formatteren) die bij elke update plaatsvinden.
Volgende stappen, in volgorde, zodat je niet heen en weer springt tussen fixes:
Als je dit bouwt met Koder.ai (koder.ai), begin in Planning Mode: definieer eerst de exacte lijstkolommen, filtervelden en API-responsevorm. Itereer daarna met snapshots en rollback als een experiment de pagina vertraagt.
Begin met het doel te veranderen van “alles laden” naar “de eerste nuttige rijen snel tonen”. Optimaliseer voor tijd tot de eerste rij en voor soepele interacties bij filteren, sorteren en scrollen, ook als de volledige dataset nooit in één keer wordt geladen.
Meet tijd tot de eerste rij na het openen of na het wijzigen van een filter, tijd voor filter/sort om te updaten, de grootte van het antwoord (JSON payload), trage databasequeries (vooral brede SELECTs en COUNT(*)) en pieken op de browser hoofdthread. Deze cijfers corresponderen direct met wat gebruikers als “lag” ervaren.
Beperk tijdelijk de API zodat deze slechts 20 rijen terugstuurt met dezelfde filters en sortering. Als het snel wordt, betaal je voornamelijk voor querykosten of payload-grootte; als het nog steeds langzaam is, ligt de bottleneck meestal bij renderen, formatteren of client-side werk per rij.
Render niet duizenden rijen tegelijk in de DOM, houd rijcomponenten simpel en geef de voorkeur aan een vaste rijhoogte. Vermijd bovendien dure formattering voor off-screen rijen; bereken en cache formattering pas wanneer een rij zichtbaar wordt of geopend is.
Virtualisatie houdt alleen de zichtbare rijen (plus een kleine buffer) gemount en hergebruikt DOM-elementen tijdens scrollen. Het is de moeite waard als gebruikers veel scrollen of rijen “zwaar” zijn, maar werkt het best wanneer rijhoogte consistent is en de tabelindeling voorspelbaar.
Paginering is voor de meeste admin- en interne workflows de veiligste standaard: het houdt gebruikers georiënteerd en beperkt serverwerk. Infinite scroll kan goed voelen voor casual browsen, maar maakt navigatie en geheugengebruik vaak lastiger tenzij je duidelijke grenzen en state-handling toevoegt.
Offset-paginering is eenvoudiger (page=10&size=50) maar wordt vaak trager bij diepe pagina’s omdat de database veel rijen moet overslaan. Keyset- of cursor-paginering blijft meestal snel omdat het doorgaat vanaf de laatst geziene rij, maar het is minder geschikt om direct naar een exact paginanummer te springen.
Start geen verzoek op elke toetsaanslag. Debounce de invoer (wacht tot de gebruiker even stopt met typen), annuleer lopende verzoeken als een nieuw verzoek begint en default naar nauwere filters (bijv. recente data of “mijn items”) zodat de eerste query klein en bruikbaar is.
Laat de API alleen de velden teruggeven die de lijst daadwerkelijk weergeeft, meestal een klein setje zoals id, label, status, eigenaar en timestamps. Zet grote tekst, JSON-blobs en gerelateerde data in een detailrequest zodat de eerste weergave licht en voorspelbaar blijft.
Laat de standaardfilter en sortering aansluiten op echt gebruik en voeg indexen toe die dat patroon ondersteunen (vaak een samengestelde index met tenant/filtervelden en de sorteerkolom). Behandel exacte totalen als optioneel: cache ze, precomputeer of laat een benadering zien zodat ze de hoofdresponse niet blokkeren.