CSS und LayoutsReact

content-visibility: Offscreen-Rendering ohne Listen-Virtualisierung

content-visibility: Offscreen-Rendering ohne Listen-Virtualisierung

Eine Liste mit tausend Kommentaren unter einem Artikel rendert merklich langsam, und das Scrollen durch sie ruckelt auf einem schwächeren Smartphone. Die Standardantwort in React ist Virtualisierung – react-window oder @tanstack/react-virtual – die im DOM nur die Elemente rendert, die gerade in den sichtbaren Bereich passen, plus einen kleinen Puffer. Das funktioniert, hat aber einen Preis, über den beim Einführen selten gesprochen wird: Elemente außerhalb des sichtbaren Bereichs sind nicht versteckt, sie existieren nicht im DOM. Strg+F findet den Text in Kommentar #850 nicht, bis du manuell dorthin scrollst. Der Seitendruck zeigt nur das, was gerade im Moment des Druckaufrufs gerendert war. Die Navigation eines Screenreaders über Überschriften oder Landmarken überspringt Inhalte, die physisch nicht im Baum existieren – das ist kein Fehler der Virtualisierung, das ist ihre unvermeidliche Konsequenz.

content-visibility greift genau dasselbe Performance-Problem von einer völlig anderen Seite an: Die Elemente bleiben im DOM, immer, vollständig. Der Browser überspringt für sie nur die Arbeit, die er normalerweise verrichten müsste – Layout, Malen, Erzeugen von Kompositions-Ebenen –, solange das Element nicht nah genug am sichtbaren Bereich ist, damit diese Arbeit überhaupt Sinn ergibt.

Was „Arbeit überspringen" genau bedeutet

content-visibility: auto auf einem Element zwingt den Browser dazu, CSS Containment darauf anzuwenden – exakt denselben Mechanismus, der im Artikel über Container Queries half, eine Endlosschleife beim Abfragen der Container-Größe zu vermeiden. Hier dient Containment einem anderen Zweck: Ist ein Element außerhalb des sichtbaren Bereichs, kann der Browser sicher annehmen, dass sein internes Layout und Malen nichts außerhalb seiner selbst beeinflussen, also kann er beides komplett aufschieben, ohne auch nur ein Pixel im Inneren neu zu berechnen.

css

.comment {
content-visibility: auto;
contain-intrinsic-size: auto 180px;
}

contain-intrinsic-size ist kein kosmetisches Extra – ohne es würde ein übersprungenes Element auf null Höhe schrumpfen, weil der Browser keine Möglichkeit hat, seine Größe zu ermitteln, wenn er das Layout im Inneren nicht berechnet. Das ist genau dasselbe Problem wie bei container-type: size ohne explizite Höhe – nur ist die Konsequenz hier nicht eine verschwindende Karte, sondern eine springende Scrollleiste, weil sich die Höhe des Dokuments ständig ändert, jedes Mal, wenn ein weiterer Kommentar in den übersprungenen Modus wechselt oder ihn verlässt. Der Wert auto 180px sagt dem Browser zwei Dinge gleichzeitig: Nimm 180px als erste Näherung, bevor das Element auch nur einmal wirklich gerendert wurde, und merke dir danach seine tatsächliche, berechnete Größe und verwende diesen gemerkten Wert als Platzhalter bei jedem weiteren Überspringen – wenn die echten Kommentare also unterschiedliche Höhen haben, schätzt der Browser mit der Zeit immer treffsicherer, wie viel Platz zu reservieren ist, statt starr an einer einzigen Zahl festzuhalten.

Ein Trick, den du bei voller Virtualisierung nicht bekommst: „hidden until found"

Hier tut content-visibility etwas, das Virtualisierung im Grunde nicht kann – nicht nur in der Praxis nicht. Ein Element im auto-Modus hat, obwohl rendertechnisch übersprungen, seinen Text weiterhin physisch im DOM. Der Browser weiß das und behandelt ein solches Element als „versteckt, aber durchsuchbar" (hidden but matchable): Wenn der Nutzer Strg+F drückt und eine Phrase eingibt, die zu Text innerhalb eines übersprungenen Elements passt, hebt der Browser das Überspringen vorübergehend auf, rendert das Element wirklich, hebt die Übereinstimmung hervor und scrollt dorthin – automatisch, ohne jeglichen Code deinerseits. display: none hat das nie getan (Strg+F ignoriert es schlicht), und eine virtualisierte Liste kann das nicht tun, weil der gesuchte Text schlicht nicht im DOM vorhanden ist, bis du manuell dorthin scrollst.

Praktisches Beispiel: eine Kommentarliste ohne eine einzige Zeile JS zur Virtualisierung

jsx

const CommentList = ({ comments }) => (
<ul className="comment-list">
{comments.map((comment) => (
<li key={comment.id} className="comment">
<p className="comment__author">{comment.author}</p>
<p className="comment__body">{comment.body}</p>
</li>
))}
</ul>
);
export default CommentList;

css

.comment {
content-visibility: auto;
contain-intrinsic-size: auto 180px;
padding: 12px 0;
border-bottom: 1px solid #e5e5e5;
}

Die gesamte Komponente rendert alle tausend <li> in den DOM – keine Logik für ein Scroll-Fenster, keine Berechnung, welche Elemente gerade sichtbar sind, keine Bibliothek. Der Browser entscheidet selbstständig, für welche der tausend Kommentare es sich gerade lohnt, Layout und Malen zu berechnen, und für welche sie rendertechnisch „nicht existieren", obwohl sie im DOM vollständig vorhanden sind.

Wann das nicht ausreicht – und warum Virtualisierung trotzdem Sinn ergibt

content-visibility löst die Kosten des Renderns – Layout und Malen. Es löst nicht die Kosten, die in einer React-Anwendung oft stärker wehtun: Jedes der tausend <li> ist immer noch eine echte React-Komponente, die gemountet und hydriert wird, eigene Hooks und Effekte haben kann. Das Überspringen des CSS-Renderns sorgt nicht dafür, dass diese tausend Komponenteninstanzen im React-Baum aufhören zu existieren, und auch nicht dafür, dass der für ihre Erzeugung nötige JavaScript-Aufwand entfällt. Für einfache, größtenteils statische Inhaltsblöcke (Kommentare, Absätze eines langen Artikels, Tabellenzeilen ohne Interaktivität) ist das unerheblich – die eigentlichen Kosten lagen ohnehin in Layout/Paint, nicht in der Komponentenlogik. Für Listen aus schweren, interaktiven Komponenten – jede mit eigenem State, Subscriptions, teurem Render – bleibt echte Virtualisierung, die nicht genutzte Instanzen vollständig aus dem React-Baum entfernt, manchmal der einzige Weg, um Flüssigkeit zu erhalten.

Fallstricke und Best Practices

  • contain-intrinsic-size ist in der Praxis Pflicht, nicht optional. Wird es weggelassen, führt das zu springendem Scrollen und einer falschen Dokumentenhöhe bei jedem Wechsel eines Elements in den übersprungenen Modus und zurück.
  • content-visibility: hidden ist nicht dasselbe wie auto. hidden überspringt das Rendern immer, unabhängig von der Position auf dem Bildschirm, bewahrt aber – anders als display: none – den internen berechneten Layout-Zustand in einem Cache: Das erneute Anzeigen des Elements (z. B. beim Umschalten eines Tabs) ist günstiger als nach display: none, weil der Browser nicht alles von Grund auf neu berechnet. Ein Element mit content-visibility: hidden wird dabei korrekt aus dem Accessibility-Baum entfernt, genau wie display: none – eine sichere Wahl für häufig umgeschaltete Tabs und Panels, nicht nur ein Performance-Trick.
  • Es ist kein Ersatz für Virtualisierung bei Listen mit schweren Komponenten. Behandle es als erstes, günstigeres Werkzeug für einfache, größtenteils statische Listen – greife erst zu echter Virtualisierung, wenn der Profiler tatsächlich Kosten auf JS-/React-Seite zeigt, nicht nur beim reinen Layout.
  • Prüfe die Unterstützung, bevor du dich voll auf „hidden but matchable" verlässt. content-visibility: auto selbst hat heute breite Unterstützung in allen wichtigen Engines, aber das Verhalten des „vorübergehenden Aufdeckens" bei Strg+F ist oft in Chromium am ausgereiftesten – betrachte es als netten Bonus, nicht als Garantie, auf die du eine Accessibility-Anforderung stützt.

Fazit

Virtualisierung und content-visibility lösen auf den ersten Blick dasselbe Problem – eine lange Liste, die zu langsam rendert –, tun das aber auf völlig unterschiedlichen Ebenen. Virtualisierung entfernt Elemente vollständig aus dem DOM und aus dem React-Baum und bezahlt dafür mit dem Verlust von Strg+F, dem Druck und Teilen der Accessibility-Navigation. content-visibility lässt den DOM vollständig bestehen und weist den Browser lediglich an, die teure Rendering-Arbeit für das, was gerade unsichtbar ist, zu überspringen – im Gegenzug bekommst du eine weniger aggressive Optimierung, dafür aber gratis, ohne etwas zu brechen, was der Browser mit dem vollständigen Dokument ohnehin schon konnte. Für die meisten langen, einigermaßen einfachen Listen ist das das richtige erste Werkzeug – Virtualisierung bleibt in Reserve für die Fälle, in denen es wirklich nicht ausreicht.