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.
Inhaltsverzeichnis
- 1. Warum lange Listen im DOM zum Performance-Problem werden
- 2. Das Prinzip der Virtualisierung: Nur sichtbare Elemente rendern
- 3. TanStack Virtual einrichten und mit Tailwind-Klassen stylen
- 4. Styling-Herausforderungen bei variabler Element-Höhe
- 5. Skeleton-Placeholder während des Scrollens gestalten
- 6. Sticky-Header in derselben virtualisierten Liste kombinieren
- 7. Overscan, Scroll-Performance und Layout-Thrashing vermeiden
- 8. Barrierefreiheit bei virtualisierten Listen
- 9. Praxis-Tipps: Wann sich Virtualisierung lohnt und wann nicht
- 10. Zusammenfassung
- 11. FAQ
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.