Lerne, wie du Dashboard‑Listen mit 100k Zeilen schnell hältst: Paginierung, Virtualisierung, smarte Filter und bessere Queries sorgen dafür, dass interne Tools reaktionsschnell bleiben.

Eine Listenansicht fühlt sich meist in Ordnung an — bis sie es nicht mehr tut. Nutzer bemerken kleine Stockungen, die sich summieren: Ruckeln beim Scrollen, die Seite bleibt nach jeder Aktualisierung kurz hängen, Filter reagieren erst nach Sekunden, und nach jedem Klick erscheint ein Spinner. Manchmal sieht der Browser‑Tab eingefroren aus, weil der UI‑Thread beschäftigt ist.
100k Zeilen sind ein häufiger Wendepunkt, weil sie alle Teile des Systems gleichzeitig belasten. Die Datenmenge ist für eine Datenbank noch normal, aber groß genug, um kleine Ineffizienzen im Browser und im Netzwerk sichtbar zu machen. Wenn du versuchst, alles gleichzeitig anzuzeigen, wird aus einem einfachen Bildschirm schnell eine schwere Pipeline.
Das Ziel ist nicht, alle Zeilen zu rendern. Das Ziel ist, jemandem zu helfen, schnell zu finden, was er braucht: die richtigen 50 Zeilen, die nächste Seite oder einen engen Ausschnitt per Filter.
Es hilft, die Arbeit in vier Bereiche zu unterteilen:
Wenn ein Teil teuer ist, wirkt sich das auf den ganzen Bildschirm aus. Eine einfache Suche kann eine Anfrage auslösen, die 100k Zeilen sortiert, tausende Datensätze zurückliefert und dann den Browser zwingt, sie alle zu rendern. So wird Tippen träge.
Wenn Teams interne Tools schnell bauen (auch mit chat‑gestützten Plattformen wie Koder.ai), sind Listenbildschirme oft der erste Ort, an dem echtes Datenwachstum die Lücke zwischen „funktioniert mit Demodaten“ und „fühlt sich jeden Tag sofort an“ offenbart.
Bevor du optimierst, entscheide, was „schnell“ für diesen Bildschirm bedeutet. Viele Teams jagen Durchsatz (alles laden), obwohl Nutzer meist niedrige Latenz brauchen (etwas schnell sehen). Eine Liste kann sich sofort anfühlen, auch wenn nie alle 100k Zeilen geladen werden, solange Scrollen, Sortieren und Filtern schnell reagieren.
Ein praktisches Ziel ist die Zeit bis zur ersten Zeile, nicht bis zur Voll‑Ladung. Nutzer vertrauen der Seite, wenn sie die ersten 20–50 Zeilen schnell sehen und Interaktionen flüssig bleiben.
Wähle eine kleine Menge Zahlen, die du bei jeder Änderung nachverfolgen kannst:
COUNT(*) und breite SELECTs)Diese Kennzahlen korrespondieren mit typischen Symptomen. Wenn die Browser‑CPU beim Scrollen ansteigt, macht das Frontend pro Zeile zu viel Arbeit. Wenn der Spinner wartet, aber das Scrollen danach in Ordnung ist, liegt das Problem meist im Backend oder im Netzwerk. Wenn die Anfrage schnell ist, die Seite aber trotzdem einfriert, ist fast immer das Rendering oder eine schwere clientseitige Verarbeitung schuld.
Probiere dieses einfache Experiment: Lass die UI unverändert, aber begrenze das Backend vorübergehend so, dass es nur 20 Zeilen mit denselben Filtern zurückgibt. Wenn es dann schnell ist, ist der Flaschenhals die Ladungsgröße oder Abfragezeit. Bleibt es langsam, schau dir Rendering, Formatierung und komponentenbezogene Arbeit pro Zeile an.
Beispiel: Eine interne Orders‑Ansicht fühlt sich beim Tippen langsam an. Wenn die API 5.000 Zeilen zurückliefert und der Browser bei jedem Tastendruck lokal filtert, wird das Tippen ruckeln. Dauert die API 2 Sekunden wegen eines COUNT auf einem nicht indexierten Filter, wartet der Nutzer, bevor sich überhaupt eine Zeile ändert. Unterschiedliche Ursachen, dieselbe Nutzerbeschwerde.
Der Browser ist oft der erste Engpass. Eine Liste kann sich langsam anfühlen, selbst wenn die API schnell ist, nur weil die Seite zu viel malen will. Die erste Regel ist einfach: Render nicht tausende Zeilen im DOM gleichzeitig.
Schon bevor du volle Virtualisierung einsetzt, halte jede Zeile leichtgewichtig. Eine Zeile mit verschachtelten Wrappern, vielen Icons, Tooltips und komplexen bedingten Styles in jeder Zelle kostet bei jedem Scrollen und Update. Bevorzuge einfachen Text, ein paar kleine Badges und nur ein oder zwei interaktive Elemente pro Zeile.
Eine stabile Zeilenhöhe hilft mehr, als es klingt. Wenn jede Zeile gleich hoch ist, kann der Browser das Layout vorhersagen und das Scrollen bleibt flüssig. Variable Höhen (umfließende Beschreibungen, aufklappbare Notizen, große Avatare) verursachen zusätzliche Messungen und Reflows. Brauchst du mehr Details, ziehe ein Seitenpanel oder einen einzelnen aufklappbaren Bereich in Betracht, nicht eine volle mehrzeilige Zeile.
Formatierung ist eine weitere stille Kostenstelle. Datums‑ und Währungsformatierung sowie aufwändige String‑Arbeit summieren sich, wenn sie in vielen Zellen wiederholt werden.
Wenn ein Wert nicht sichtbar ist, berechne ihn noch nicht. Cache teure Formatierungsergebnisse und berechne sie bei Bedarf, z. B. wenn eine Zeile sichtbar wird oder ein Nutzer sie öffnet.
Ein schneller Durchlauf, der oft deutlich hilft:
Beispiel: Eine interne Rechnungs‑Tabelle mit 12 Währungs‑ und Datumsfeldern ruckelt beim Scrollen. Das Cachen formatierter Werte pro Rechnung und das Verzögern von Arbeit für Off‑Screen‑Zeilen kann das Gefühl von Instantaneität liefern, noch bevor tiefere Backend‑Arbeiten erfolgen.
Virtualisierung bedeutet, dass die Tabelle nur die Zeilen zeichnet, die man tatsächlich sehen kann (plus kleinen Puffer oben und unten). Beim Scrollen werden dieselben DOM‑Elemente wiederverwendet und die Daten darin ausgetauscht. So versucht der Browser nicht, Zehntausende von Zeilenkomponenten gleichzeitig zu zeichnen.
Virtualisierung passt gut bei langen Listen, breiten Tabellen oder „schweren“ Zeilen (Avatare, Status‑Chips, Aktionsmenüs, Tooltips). Sie ist auch nützlich, wenn Nutzer viel scrollen und eine kontinuierliche Ansicht erwarten statt seitenweiser Sprünge.
Es ist kein Zauber. Einige Dinge sorgen oft für Überraschungen:
Der schlichteste Ansatz ist langweilig: feste Zeilenhöhe, vorhersehbare Spalten und nicht zu viele interaktive Widgets pro Zeile.
Beides lässt sich kombinieren: nutze Paginierung (oder cursor‑basiertes „Load more“), um zu begrenzen, was du vom Server holst, und Virtualisierung, um das Rendering innerhalb dieses Slice günstig zu halten.
Ein praktisches Muster ist, eine normale Seitengröße (oft 100–500 Zeilen) zu laden, innerhalb dieser Seite zu virtualisieren und klare Steuerungen zum Seitenwechsel anzubieten. Bei Infinite Scroll füge einen sichtbaren „Geladen X von Y“‑Indikator hinzu, damit Nutzer verstehen, dass sie nicht alles sehen.
Wenn du eine Listenansicht brauchst, die mit wachsender Datenmenge nutzbar bleibt, ist Paginierung meist die sicherste Default‑Wahl. Sie ist vorhersehbar, passt gut zu Admin‑Workflows (prüfen, bearbeiten, genehmigen) und unterstützt Bedürfnisse wie „Seite 3 mit diesen Filtern exportieren“ ohne Überraschungen. Viele Teams landen wieder bei Paginierung, nachdem sie fancieres Scrollen getestet haben.
Infinite Scroll kann für entspanntes Browsen schön wirken, hat aber versteckte Kosten. Nutzer verlieren oft die Orientierung, der Zurück‑Button kehrt nicht immer an dieselbe Stelle zurück, und lange Sessions können Speicherprobleme bringen, wenn immer mehr Zeilen geladen werden. Ein Mittelweg ist ein "Load more"‑Button, der trotzdem mit Seiten arbeitet, sodass Nutzer orientiert bleiben.
Offset‑Paginierung ist der klassische page=10&size=50‑Ansatz. Er ist simpel, kann aber bei großen Tabellen langsamer werden, weil die DB viele Zeilen überspringen muss, um zu späteren Seiten zu gelangen. Auch kann es merkwürdig wirken, wenn neue Zeilen eintreffen und Elemente zwischen Seiten verschoben werden.
Keyset‑Paginierung (oft Cursor genannt) fragt nach „den nächsten 50 Zeilen nach dem zuletzt gesehenen Element“, typischerweise unter Nutzung einer id oder von created_at. Sie bleibt meist schnell, weil sie nicht so viel zählen und überspringen muss.
Eine praktische Regel:
Nutzer mögen Totals sehen, aber ein volles „count all matching rows“ kann mit schweren Filtern teuer sein. Optionen sind: Counts für populäre Filter cachen, die Zählung im Hintergrund nachladen, oder eine ungefähre Zahl anzeigen (z. B. „10.000+“).
Beispiel: Eine Orders‑Ansicht kann sofort Ergebnisse mit Keyset‑Paginierung zeigen und die exakte Gesamtanzahl erst dann füllen, wenn der Nutzer kurz mit dem Filtern aufgehört hat.
Wenn du das in Koder.ai baust, behandle Paginierungs‑ und Count‑Verhalten früh als Teil der Screen‑Spec, damit generierte Backend‑Queries und UI‑State sich später nicht ins Gehege kommen.
Die meisten Listen wirken langsam, weil sie zu offen starten: alles laden und den Nutzer dann einschränken lassen. Dreh das um. Starte mit sinnvollen Defaults, die eine kleine, nützliche Menge zurückgeben (z. B. Letzte 7 Tage, Meine Einträge, Status: Offen), und mache „Alle Zeiten“ zur expliziten Wahl.
Textsuche ist eine weitere Falle. Wenn du bei jedem Tastenanschlag eine Abfrage ausführst, erzeugst du einen Rückstau von Requests und eine flackernde UI. Debounciere die Suche, sodass erst nach einer kurzen Pause eine Abfrage erfolgt, und breche ältere Requests ab, wenn ein neuer startet. Einfache Regel: Wenn der Nutzer noch tippt, frag nicht den Server an.
Filtering fühlt sich nur dann schnell an, wenn es auch klar ist. Zeige Filter‑Chips oben in der Tabelle, damit Nutzer sehen, was aktiv ist und es mit einem Klick entfernen können. Halte Chip‑Labels menschenlesbar, nicht rohe Feldnamen (z. B. Owner: Sam statt owner_id=42). Wenn jemand sagt „meine Ergebnisse sind verschwunden“, liegt es meist an einem unsichtbaren Filter.
Muster, die große Listen reaktionsfähig halten, ohne die UI zu verkomplizieren:
Gespeicherte Ansichten sind oft die stille Heldin. Statt Nutzern beizubringen, jedes Mal die perfekte Einmal‑Filterkombination zu bauen, gib ihnen einige Presets, die echte Workflows abdecken. Ein Ops‑Team wechselt vielleicht zwischen „Fehlgeschlagene Zahlungen heute“ und „High‑Value Kunden“. Diese sind mit einem Klick verständlich und leichter backend‑freundlich schnell zu halten.
Wenn du ein internes Tool in einem chat‑getriebenen Builder wie Koder.ai baust, betrachte Filter als Teil des Produktflusses, nicht als Anbauteil. Starte mit den häufigsten Fragen und entwirf Default‑ und Saved‑Views darum herum.
Eine Listenansicht braucht selten dieselben Daten wie eine Detailseite. Wenn deine API alles über alles zurückgibt, zahlst du doppelt: die DB macht mehr Arbeit und der Browser empfängt und rendert mehr als nötig. Query‑Shaping heißt, nur das zu fordern, was die Liste gerade braucht.
Gib zuerst nur die Spalten zurück, die jede Zeile rendert. Für die meisten Dashboards sind das id, einige Labels, Status, Besitzer und Zeitstempel. Große Texte, JSON‑Blobs und berechnete Felder können warten, bis der Nutzer eine Zeile öffnet.
Vermeide schwere Joins für den ersten Paint. Joins sind in Ordnung, wenn sie Indexe nutzen und kleine Ergebnisse liefern, aber teuer, wenn du mehrere Tabellen joinst und dann auf den verbundenen Daten sortierst oder filterst. Ein einfaches Muster: Liste schnell aus einer Tabelle holen, verwandte Details on‑demand laden (oder für sichtbare Zeilen batchen).
Begrenze die Sortieroptionen auf indexierte Spalten. „Nach allem sortieren“ klingt hilfreich, führt aber oft zu langsamen Sorts auf großen Datensätzen. Bevorzuge einige vorhersehbare Optionen wie created_at, updated_at oder status und stelle sicher, dass diese Spalten indexiert sind.
Sei vorsichtig bei serverseitiger Aggregation. COUNT(*) auf einer großen gefilterten Menge, DISTINCT auf einer breiten Spalte oder Berechnungen für Gesamtseiten können die Antwortzeit dominieren.
Praktischer Ansatz:
COUNT und DISTINCT als optional, cache oder approximier sie wenn möglichWenn du interne Tools auf Koder.ai baust, definiere eine leichte Listenquery separat von der Details‑Query in der Planungsphase, damit die UI snappy bleibt, wenn die Daten wachsen.
Wenn du eine Listenansicht willst, die bei 100k Zeilen schnell bleibt, muss die Datenbank pro Anfrage weniger Arbeit machen. Die meisten langsamen Listen sind kein Problem der Datenmenge, sondern des falschen Datenzugriffs.
Starte mit Indizes, die dem entsprechen, was Nutzer tatsächlich tun. Filterst du meist nach status und sortierst nach created_at, dann brauchst du einen Index, der beides in dieser Reihenfolge unterstützt. Ansonsten scannt die DB unter Umständen viel mehr Zeilen und sortiert sie, was schnell teuer wird.
Fixes mit größtem Effekt:
tenant_id, status, created_at).OFFSET‑Seiten. OFFSET lässt die DB viele Zeilen überspringen.Einfaches Beispiel: Eine Orders‑Tabelle zeigt Kundenname, Status, Betrag und Datum. Joine nicht jede verwandte Tabelle und ziehe nicht die gesamten Bestell‑Notes für die Listensicht. Gib nur die für die Tabelle genutzten Spalten zurück und lade den Rest separat, wenn der Nutzer eine Bestellung anklickt.
Wenn du mit einer Plattform wie Koder.ai baust, behalte dieses Mindset, auch wenn die UI aus Chat generiert wird. Sorge dafür, dass die generierten API‑Endpoints Cursor‑Paginierung und selektive Felder unterstützen, damit die DB‑Arbeit bei wachsendem Table‑Size vorhersehbar bleibt.
Wenn eine Listen‑Seite heute langsam wirkt, fang nicht mit einem vollständigen Rewrite an. Bestimme zuerst, wie normale Nutzung aussieht, und optimiere diesen Pfad.
Default‑View definieren. Wähle Standardfilter, Sortierung und sichtbare Spalten. Listen werden langsam, wenn sie standardmäßig versuchen, alles zu zeigen.
Paging‑Stil wählen, der zur Nutzung passt. Wenn Nutzer meist die ersten Seiten scannen, ist klassische Paginierung okay. Springen sie weit (Seite 200+) oder brauchst du konstante Performance, nutze Keyset‑Paginierung (z. B. created_at plus id).
Virtualisierung für den Tabellenkörper hinzufügen. Selbst bei schnellem Backend kann der Browser bei zu vielen Zeilen choke‑n.
Suche und Filter reaktionsschnell machen. Debounce beim Tippen, damit nicht bei jedem Keypress eine Anfrage losgeht. Halte Filterstate in der URL oder in einem gemeinsamen State‑Store, damit Refresh, Back‑Button und Teilen zuverlässig funktionieren. Cache das letzte erfolgreiche Ergebnis, damit die Tabelle nicht leer aufblinkt.
Messen, dann Queries und Indizes tunen. Logge Server‑Zeit, DB‑Zeit, Payload‑Größe und Render‑Zeit. Dann straffe die Query: wähle nur die Spalten, die du zeigst, filtere früh und füge Indizes passend zum Default‑Filter+Sort hinzu.
Beispiel: Ein internes Support‑Dashboard mit 100k Tickets. Default: Offen, meinem Team zugewiesen, nach Neueste sortiert, sechs Spalten anzeigen und nur Ticket‑id, Betreff, Bearbeiter, Status und Zeitstempel holen. Mit Keyset‑Paginierung und Virtualisierung bleiben DB und UI vorhersehbar.
Wenn du in Koder.ai baust, passt dieser Plan gut zu einem Iterate‑and‑Check‑Workflow: Ansicht anpassen, Scroll‑ und Suchverhalten testen, dann die Query optimieren, bis die Seite snappy bleibt.
Der schnellste Weg, eine Listenansicht zu ruinieren, ist, 100k Zeilen wie eine normale Seite zu behandeln. Die meisten langsamen Dashboards folgen ein paar typischen Fallen.
Eine große ist, alles zu rendern und mit CSS zu verstecken. Auch wenn nur 50 Zeilen sichtbar scheinen, bezahlt der Browser trotzdem für das Erstellen von 100k DOM‑Knoten, das Messen und Repainten beim Scrollen. Wenn du lange Listen brauchst, rendere nur das, was sichtbar ist (Virtualisierung) und halte Zeilenkomponenten einfach.
Suche kann Performance still sabotieren, wenn jeder Tastendruck einen Full‑Table‑Scan auslöst. Das passiert, wenn Filter nicht indexiert sind, wenn du über zu viele Spalten suchst oder Contains‑Queries auf großen Textfeldern ohne Plan ausführst. Gute Regel: Der erste Filter, zu dem Nutzer greifen, sollte in der DB günstig sein, nicht nur in der UI bequem.
Ein weiterer häufiger Fehler ist, ganze Datensätze zu holen, obwohl die Liste nur Zusammenfassungen braucht. Eine Zeile braucht meist 5–12 Felder, nicht das ganze Objekt, keine langen Beschreibungen und keine verwandten Daten. Extra Daten zu ziehen erhöht DB‑Arbeit, Netzwerkzeit und Frontend‑Parsing.
Export und Totals können die UI einfrieren, wenn du sie im Main‑Thread berechnest oder auf eine schwere Anfrage wartest. Halte die UI interaktiv: Starte Exporte im Hintergrund, zeige Fortschritt und vermeide Totals bei jeder Filteränderung neu zu berechnen.
Zu viele Sortoptionen schaden ebenfalls. Wenn Nutzer nach jeder Spalte sortieren können, wirst du große Resultsets in Memory sortieren müssen oder die DB in langsame Pläne zwingen. Beschränke Sorts auf wenige indexierte Spalten und sorge dafür, dass die Default‑Sortierung zu einem realen Index passt.
Schnelle Orientierungshilfe:
Behandle Listen‑Performance wie ein Produktfeature, nicht als einmalige Schrauberei. Eine Listenansicht ist nur dann schnell, wenn sie sich beim echten Scrollen, Filtern und Sortieren mit echten Nutzern schnell anfühlt.
Nutze diese Checkliste, um zu bestätigen, dass du die richtigen Dinge gefixt hast:
Ein einfacher Reality‑Check: Öffne die Liste, scrolle 10 Sekunden, dann wende einen häufigen Filter an (z. B. Status: Offen). Friert die UI ein, liegt das Problem meist am Rendering (zu viele DOM‑Zeilen) oder an einer schweren clientseitigen Transformation (Sortieren, Gruppieren, Formatieren), die bei jedem Update läuft.
Nächste Schritte, in Reihenfolge, damit du nicht zwischen Fixes hin‑und‑her springst:
Wenn du das mit Koder.ai (koder.ai) baust, beginne im Planning Mode: definiere erst die exakten Listen‑Spalten, Filterfelder und die API‑Antwortstruktur. Iteriere dann mit Snapshots und rolle zurück, wenn ein Experiment die Seite verlangsamt.
Ändere das Ziel von „alles laden“ zu „die ersten n nützlichen Zeilen schnell zeigen“. Optimiere auf die Zeit bis zur ersten Zeile und auf flüssige Interaktionen beim Filtern, Sortieren und Scrollen – auch wenn nie alle 100k Zeilen gleichzeitig geladen werden.
Miss Zeit bis zur ersten Zeile nach Laden oder Filteränderung, Zeit für Filter/Sort, Payload‑Größe, langsame DB‑Abfragen (insbesondere breite SELECTs und COUNT(*)) und Main‑Thread‑Spitzen im Browser. Diese Kennzahlen spiegeln direkt das Wahrgenommene „Lag“ wider.
Begrenze die API vorübergehend auf nur 20 Zeilen bei gleichen Filtern und Sortierung. Wird es schnell, liegt das Problem meist an Abfragekosten oder Payload‑Größe. Bleibt es langsam, ist Rendering, Formatierung oder pro‑Zeile‑Aufwand im Client wahrscheinlich die Ursache.
Render nie tausende DOM‑Zeilen gleichzeitig, halte Zeilenkomponenten einfach und bevorzuge eine feste Zeilenhöhe. Vermeide teures Formatieren für nicht sichtbare Zeilen – berechne und cache solche Werte nur, wenn eine Zeile sichtbar ist oder geöffnet wird.
Virtualisierung mountet nur die sichtbaren Zeilen (plus kleinen Puffer) und reused DOM‑Elemente beim Scrollen. Das lohnt sich, wenn Nutzer viel scrollen oder Zeilen „schwer“ sind. Am besten funktioniert sie bei konstanter Zeilenhöhe und vorhersehbarem Layout.
Paginierung ist für die meisten Admin‑Workflows die sicherere Default‑Wahl: sie hält Nutzer orientiert und begrenzt Server‑Arbeit. Infinite Scroll kann für lockeres Browsen passen, bringt aber Probleme bei Navigation, Back‑Button‑Verhalten und Speicher, wenn man nicht limitiert.
Offset‑Paginierung (page=10&size=50) ist einfacher, kann aber bei tiefen Seiten langsam werden, weil die DB viele Reihen überspringt. Keyset (Cursor)‑Paginierung lädt die nächsten 50 Zeilen nach dem zuletzt gesehenen Datensatz und bleibt normalerweise schneller, ist aber weniger gut geeignet, wenn man exakt zu Seite X springen muss.
Führe nicht bei jedem Tastendruck eine Anfrage aus. Debounce das Suchfeld, breche laufende Requests ab, wenn ein neuer startet, und starte standardmäßig mit einschränkenden Filtern (z. B. letzter 7 Tage, Meine Einträge), damit die erste Anfrage klein und nützlich ist.
Lass die List‑API nur die Felder zurückgeben, die für die Anzeige nötig sind: meist id, ein Label, status, owner und Zeitstempel. Große Texte, JSON‑Blobs und verwandte Daten erst in einer Detail‑Anfrage laden, damit die erste Darstellung leicht bleibt.
Passe Filter und Sort an das Nutzerverhalten an und lege passende Indizes an. Oft hilft ein zusammengesetzter Index, der gängige Filter und Sort kombiniert (z. B. tenant_id, status, created_at). Exakte Totals können teuer sein – cachen, vorab berechnen oder nur approximieren (z. B. „10.000+“) statt sie immer live zu berechnen.