Virtual Scrolling mit Tailwind: Grosse Listen performant stylen
AI generated
tw
Tailwind CSS · Performance · TanStack Virtual
Virtual Scrolling: Große Listen mit Tailwind performant stylen
Nur sichtbare Elemente rendern, ohne dass das Styling zerbricht

Eine Liste mit mehreren tausend Einträgen bringt jeden Browser ins Schwitzen, sobald jeder Eintrag ein eigener DOM-Knoten mit Tailwind-Klassen für Padding, Border und Hover-States ist. Virtualisierung löst das Problem, indem nur die aktuell sichtbaren Elemente tatsächlich gerendert werden, bringt dabei aber eigene Styling-Herausforderungen mit sich, von variabler Item-Höhe bis zu Sticky-Headers innerhalb der virtualisierten Liste.

16 Min. Lesezeit TanStack Virtual · Overscan · measureElement Skeleton-Loader · Sticky Header · a11y

1. Warum lange Listen im DOM zum Performance-Problem werden

Eine Produktliste mit fünftausend Zeilen erzeugt, sobald jede Zeile mehrere verschachtelte div-Elemente für Bild, Titel, Badge und Aktionen enthält, schnell mehrere zehntausend DOM-Knoten. Der Browser muss für jeden dieser Knoten Style-Berechnung, Layout und Paint durchführen, selbst wenn nur ein Bruchteil davon gerade im sichtbaren Viewport liegt. Das Ergebnis sind spürbar längere Ladezeiten, ein träges Scrollverhalten und ein Speicherverbrauch, der linear mit der Listenlänge wächst.

Besonders auf schwächerer Hardware oder mobilen Geräten summiert sich das schnell zu einer spürbaren Verzögerung zwischen Interaktion und Reaktion der Seite. Selbst wenn jede einzelne Tailwind-Klasse für sich genommen trivial ist, addiert sich der Berechnungsaufwand über zehntausende Knoten zu einem echten Engpass. Genau an diesem Punkt setzt Virtualisierung an, indem sie das Problem an der Wurzel angeht statt es nur zu kaschieren.

2. Das Prinzip der Virtualisierung: Nur sichtbare Elemente rendern

Eine virtualisierte Liste rendert zu jedem Zeitpunkt nur die Elemente, die tatsächlich im sichtbaren Fenster liegen, plus einen kleinen Puffer außerhalb davon. Die restliche, nicht gerenderte Höhe wird über einen Platzhalter-Container mit berechneter Gesamthöhe simuliert, damit die native Scrollbar weiterhin proportional korrekt anzeigt, wie weit man sich in der Liste befindet.

Bibliotheken wie TanStack Virtual übernehmen dabei die Messung der Item-Höhen, die Positionierung über transform: translateY und das Wiederverwenden von DOM-Knoten beim Scrollen. Tailwind bleibt in diesem Modell weiterhin ausschließlich für das visuelle Styling zuständig, während die eigentliche Positionierungslogik komplett in JavaScript über Inline-Styles läuft, die von der Bibliothek berechnet werden.

3. TanStack Virtual einrichten und mit Tailwind-Klassen stylen

Der useVirtualizer-Hook von TanStack Virtual benötigt im Kern drei Angaben: die Gesamtanzahl der Elemente, eine Referenz auf das scrollende Element und eine Schätzfunktion für die Item-Höhe. Der äußere Container erhält eine feste Höhe sowie overflow-y-auto als Tailwind-Klasse, während der innere Container die vom Virtualizer berechnete Gesamthöhe als Inline-Style bekommt, damit die Scrollbar korrekt dimensioniert ist.

Jedes einzelne virtuelle Item wird absolut positioniert und über ein transform an seine berechnete Position verschoben, das visuelle Styling wie Rahmen, Abstand und Hover-Zustand kommt dabei vollständig aus normalen Tailwind-Utility-Klassen. Diese klare Trennung zwischen Positionierung durch die Bibliothek und Optik durch Tailwind macht die Komponente wartbar, weil Design-Aenderungen nie die Virtualisierungslogik berühren müssen.


import { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';

function ProductList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 72,
    overscan: 6,
  });

  return (
    <div ref={parentRef} class="h-[600px] overflow-y-auto rounded-lg border border-gray-200">
      <div
        class="relative w-full"
        style={{ height: `${virtualizer.getTotalSize()}px` }}
      >
        {virtualizer.getVirtualItems().map((row) => (
          <div
            key={row.key}
            class="absolute left-0 top-0 flex w-full items-center gap-3 border-b border-gray-100 px-4 py-3 hover:bg-gray-50"
            style={{ height: `${row.size}px`, transform: `translateY(${row.start}px)` }}
          >
            <span class="font-medium text-gray-900">{items[row.index].name}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

4. Styling-Herausforderungen bei variabler Element-Höhe

Sobald Items nicht alle gleich hoch sind, etwa bei Kommentaren mit unterschiedlich langem Text, reicht eine feste Schätzung nicht mehr aus. Der Virtualizer misst dann nach dem Rendern über measureElement die tatsächliche Höhe jedes Knotens nach und korrigiert seine internen Positionsberechnungen entsprechend, was einen kurzen zusätzlichen Renderdurchlauf verursacht.

Für verlässliches Messen dürfen die gerenderten Items keine externen margin-Werte über Tailwind-Klassen wie mb-4 tragen, weil Margins nicht zur gemessenen Bounding Box zählen und die Positionsberechnung dadurch systematisch abweicht. Stattdessen gehört der Abstand zwischen Items als padding auf einen inneren Wrapper oder als border-bottom, damit die gemessene Höhe exakt der Höhe entspricht, die der Virtualizer für seine Berechnung erwartet.

5. Skeleton-Placeholder während des Scrollens gestalten

Bei serverseitig paginierten Daten kann schnelles Scrollen dazu führen, dass ein Nutzer einen Bereich erreicht, dessen Daten noch nicht geladen sind. Statt eines leeren, weißen Bereichs sollte hier ein Skeleton-Platzhalter mit animate-pulse und grauen Balken erscheinen, damit klar signalisiert wird, dass Inhalt nachgeladen wird und die Liste nicht einfach zu Ende ist.

Entscheidend ist, dass der Skeleton exakt dieselbe Höhe wie das später geladene reale Item hat, sonst springt beim Nachladen plötzlich der gesamte restliche Inhalt der Liste nach oben oder unten. Bewährte Tailwind-Klassen dafür sind etwa h-16 rounded bg-gray-200 dark:bg-gray-700 animate-pulse, kombiniert mit denselben Abstandsklassen wie beim echten Item.

6. Sticky-Header in derselben virtualisierten Liste kombinieren

Gruppierte Listen, etwa ein Kontaktverzeichnis mit Buchstaben-Trennern, brauchen einen Section-Header, der beim Scrollen oben hängen bleibt. Da der Virtualizer nur die aktuell sichtbare Range an Items kennt, muss dieser Header separat berechnet und als eigenes, mit sticky top-0 z-10 positioniertes Element außerhalb des normalen virtuellen Item-Flows gerendert werden.

Das gängige Muster dafür bestimmt bei jedem Scroll-Frame den Index des zuletzt passierten Gruppen-Headers und rendert diesen zusätzlich fixiert über der eigentlichen Liste, während die normalen Items weiterhin ganz gewöhnlich durchscrollen. So bleibt die Orientierung erhalten, obwohl technisch nur ein kleiner Ausschnitt der Gesamtliste tatsächlich im DOM existiert.

7. Overscan, Scroll-Performance und Layout-Thrashing vermeiden

Der Overscan-Parameter legt fest, wie viele zusätzliche Items außerhalb des sichtbaren Bereichs vorgerendert werden. Ein zu niedriger Wert führt bei schnellem Scrollen zu kurzzeitig leeren Fenstern (Blanking), ein zu hoher Wert vergrößert wieder unnötig die Anzahl der DOM-Knoten und damit den Vorteil der Virtualisierung. In der Praxis liefern Werte zwischen vier und acht Items meist ein gutes Gleichgewicht.

Layout-Thrashing entsteht, wenn Lesen (etwa getBoundingClientRect) und Schreiben von Layout-Eigenschaften im selben Frame abwechselnd passieren und der Browser dadurch wiederholt neu layouten muss. Höhe-Messungen über measureElement sollten deshalb gebündelt und nicht bei jedem einzelnen Scroll-Event synchron ausgeführt werden, um diese teuren Forced-Reflow-Zyklen zu vermeiden.

8. Barrierefreiheit bei virtualisierten Listen

Ein Screenreader nimmt nur die Elemente wahr, die aktuell tatsächlich im DOM stehen, nicht die vollständige logische Liste. Die Attribute aria-setsize und aria-posinset auf jedem gerenderten Item kommunizieren dem Screenreader trotzdem die tatsächliche Gesamtgröße und Position, auch wenn physisch nur ein kleiner Ausschnitt existiert, ergänzt durch role="list" auf dem Container.

Bei Tastatur-Navigation mit Pfeiltasten muss der Scrollcontainer automatisch mitscrollen, sobald das fokussierte Element den sichtbaren Bereich verlässt, sonst wandert der Fokus unsichtbar aus dem Blickfeld und die Anwendung wirkt für Tastaturnutzer defekt. Diese Synchronisation zwischen Fokus und Scrollposition muss explizit in der Virtualisierungslogik behandelt werden, sie entsteht nicht automatisch.

9. Praxis-Tipps: Wann sich Virtualisierung lohnt und wann nicht

Als Faustregel lohnt sich Virtualisierung merklich, sobald eine Liste dauerhaft mehr als etwa zwei- bis dreihundert DOM-Knoten erzeugen würde. Darunter überwiegt oft der zusätzliche Implementierungs- und Wartungsaufwand für Messlogik, Sticky-Header-Sonderfälle und Accessibility-Anpassungen den tatsächlichen Performance-Gewinn, den man in der Praxis kaum noch spürt.

Für einfachere Fälle kann die CSS-Eigenschaft content-visibility: auto bereits einen Teil des Effekts liefern, ohne dass eine JavaScript-Bibliothek nötig ist, allerdings ohne die feingranulare Kontrolle über Sticky-Header oder variable Höhen. Bei suchmaschinenrelevanten Inhalten sollte Virtualisierung generell vermieden werden, weil Crawler nur den initial gerenderten Ausschnitt sehen, hier sind Pagination oder vollständiges Server-Side-Rendering die bessere Wahl.

Ansatz DOM-Knoten bei 5000 Items Bibliotheksgröße Eignung
Keine Virtualisierung über 5000 Knoten 0 KB Kleine Listen, einfachste Umsetzung
TanStack Virtual ca. 20 bis 40 Knoten ca. 4 KB Große dynamische Listen, variable Höhe
react-window ca. 20 bis 40 Knoten ca. 6 KB Feste Item-Höhe, schlanke API
CSS content-visibility: auto alle Knoten vorhanden, aber nicht gerendert 0 KB Mittelgroße Listen ohne JS-Abhängigkeit

Mironsoft

Tailwind-CSS-Architektur, Design-Systeme und Performance

Tailwind-Frontends, die trotz tausender Utility-Klassen wartbar bleiben?

Wir prüfen bestehende Tailwind-Projekte auf aufgeblähte Klassenlisten, inkonsistente Design-Tokens und ungenutzte CSS-Reste und bauen daraus ein Design-System, das sich sauber skaliert statt mit jeder Komponente unübersichtlicher zu werden.

Design-System-Review

Tokens, Spacing-Skala und Komponentenkonsistenz auf Wartbarkeit prüfen.

Performance-Optimierung

CSS-Bundle-Größe, Purge-Konfiguration und Ladezeiten systematisch reduzieren.

Component-Architektur

Wiederverwendbare, gut strukturierte Komponenten statt Klassenlisten-Wildwuchs aufbauen.

10. Zusammenfassung

Virtual Scrolling mit Tailwind: Das Wichtigste auf einen Blick

Overscan-Wert

Vier bis acht zusätzliche Items als Puffer verhindern Blanking beim schnellen Scrollen.

Item-Höhe

Feste Höhe ist am performantesten, variable Höhe braucht measureElement zum Nachmessen.

Barrierefreiheit

aria-setsize und aria-posinset kommunizieren die logische Listenposition an Screenreader.

SEO-Hinweis

Virtualisierte Inhalte sind für Crawler unsichtbar, bei SEO-Relevanz lieber Pagination nutzen.

11. FAQ: Virtual Scrolling mit Tailwind: Das Wichtigste auf einen Blick

1Ab wann lohnt sich Virtual Scrolling überhaupt?
Als Faustregel ab etwa zwei- bis dreihundert dauerhaft gerenderten DOM-Knoten. Darunter überwiegt meist der zusätzliche Implementierungsaufwand den spürbaren Performance-Gewinn im Alltag.
2Funktioniert Virtualisierung auch ohne TanStack Virtual?
Ja, das Prinzip lässt sich auch selbst umsetzen, etwa über den Intersection Observer und manuelle Positionsberechnung. Bibliotheken sparen dabei vor allem die Fehlerquellen bei Messung und Recycling der DOM-Knoten.
3Was passiert mit der Browser-Suche (Strg+F) bei virtualisierten Listen?
Die Browser-Suche findet nur Text, der aktuell im DOM steht, also nicht die komplette Liste. Für durchsuchbare Inhalte sollte deshalb eine eigene Such- oder Filterfunktion angeboten werden statt sich auf die native Browsersuche zu verlassen.
4Wie wirkt sich Virtualisierung auf SEO aus?
Suchmaschinen-Crawler sehen in der Regel nur den initial gerenderten Ausschnitt der Liste, nicht die vollständigen Daten. Für indexierbare Inhalte ist deshalb Pagination oder vollständiges Server-Side-Rendering vorzuziehen.
5Kann man Virtualisierung mit Infinite Scroll kombinieren?
Ja, das ist ein gängiges Muster. Der Virtualizer liefert dabei über ein Callback den Index des zuletzt sichtbaren Items, bei dessen Erreichen die nächste Datenseite nachgeladen und die Gesamtanzahl der Items erhöht wird.
6Wie behebt man Ruckeln beim schnellen Scrollen?
Meist hilft ein höherer Overscan-Wert sowie das Vermeiden synchroner Layout-Messungen bei jedem Scroll-Event. Auch teure CSS-Effekte wie Schatten oder Blur auf jedem einzelnen Item sollten in virtualisierten Listen sparsam eingesetzt werden.
7Braucht man für horizontale Listen eine andere Bibliothek?
Nein, TanStack Virtual unterstützt über den Parameter horizontal sowohl vertikales als auch horizontales Scrollen mit derselben API. Die Positionierung wechselt dabei intern von translateY auf translateX.
8Wie testet man virtualisierte Listen automatisiert?
End-to-End-Tests sollten prüfen, dass nach programmatischem Scrollen die erwarteten Items im DOM erscheinen, statt die komplette Liste auf einmal zu erwarten. Unit-Tests für die Höhen-Schätzfunktion fangen zusätzlich Regressionen bei variablen Item-Größen ab.
9Was ist der Unterschied zwischen Virtualisierung und Pagination?
Pagination lädt und zeigt jeweils nur eine begrenzte Seite an Daten, Virtualisierung hält dagegen alle Daten im Speicher, rendert aber nur den sichtbaren Ausschnitt als DOM. Beide Ansätze lassen sich auch kombinieren.
10Funktioniert content-visibility als Alternative zu JS-Virtualisierung?
Für einfachere Fälle ja, da der Browser das Rendering außerhalb des Viewports selbst überspringt. Feinsteuerung wie Sticky-Header oder exaktes Overscan-Verhalten bietet die reine CSS-Lösung allerdings nicht.