css en layoutsreact

content-visibility: offscreen renderen zonder lijstvirtualisatie

content-visibility: offscreen renderen zonder lijstvirtualisatie

Een lijst van duizend reacties onder een artikel rendert merkbaar traag, en scrollen erdoorheen hapert op een zwakkere telefoon. Het standaardantwoord in React is virtualisatie – react-window of @tanstack/react-virtual – die alleen de elementen die toevallig binnen het zichtbare venster passen in de DOM rendert, plus een kleine buffer. Het werkt, maar het heeft een kostenpost waar bij implementatie zelden over gesproken wordt: elementen buiten het venster zijn niet verborgen, ze bestaan niet in de DOM. Ctrl+F indrukken vindt geen tekst in reactie #850, totdat je er handmatig naartoe scrolt. Het afdrukken van de pagina toont alleen wat er toevallig gerenderd was op het moment dat je op afdrukken drukte. Navigatie van een schermlezer via koppen of landmarks slaat inhoud over die fysiek niet in de boom bestaat – dat is geen bug van virtualisatie, het is een onvermijdelijk gevolg ervan.

content-visibility valt hetzelfde performanceprobleem aan vanuit een compleet andere hoek: de elementen blijven in de DOM, altijd, volledig. De browser slaat alleen het werk over dat hij er normaal voor zou moeten doen – layout, paint, het aanmaken van compositorlagen – totdat het element dicht genoeg bij het zichtbare gebied komt om dat werk zinvol te maken.

Wat "werk overslaan" precies betekent

content-visibility: auto op een element dwingt de browser om er CSS Containment op toe te passen – exact hetzelfde mechanisme dat in het artikel over Container Queries een lus voorkwam bij het bevragen van het formaat van de container. Hier dient containment een ander doel: als een element offscreen is, mag de browser er veilig van uitgaan dat de interne layout en het paintwerk ervan niets buiten zichzelf beïnvloeden, dus kan hij ze volledig uitstellen, zonder ook maar één pixel erbinnen te berekenen.

css

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

contain-intrinsic-size is geen cosmetisch extraatje – zonder deze regel zou een overgeslagen element krimpen naar nul hoogte, omdat de browser geen formaat kan afleiden als hij de layout erbinnen niet berekent. Dat is precies hetzelfde probleem als bij container-type: size zonder expliciete hoogte – alleen is het gevolg hier geen verdwijnende kaart, maar een springende scrollbalk, omdat de hoogte van het document plotseling verandert elke keer dat een volgende reactie de overgeslagen modus in- of uitgaat. De waarde auto 180px vertelt de browser twee dingen tegelijk: gebruik 180px als eerste schatting, voordat het element ook maar één keer echt is gerenderd, en onthoud daarna zijn werkelijke, berekende formaat en gebruik die onthouden waarde als placeholder bij elke volgende keer dat het wordt overgeslagen – dus als echte reacties verschillende hoogtes hebben, raadt de browser met de tijd steeds nauwkeuriger hoeveel ruimte hij moet reserveren, in plaats van star bij één getal te blijven.

Een truc die je bij volledige virtualisatie niet krijgt: hidden until found

Dit is het punt waarop content-visibility iets doet wat virtualisatie in principe niet kan – niet alleen in de praktijk. Een element in auto-modus heeft, ondanks dat het renderend wordt overgeslagen, zijn tekst nog steeds fysiek in de DOM staan. De browser weet dit en behandelt zo'n element als "verborgen maar doorzoekbaar" (hidden but matchable): wanneer een gebruiker Ctrl+F indrukt en een zin typt die overeenkomt met tekst binnen een overgeslagen element, draait de browser de overslag tijdelijk terug, rendert het element echt, markeert de treffer en scrolt ernaartoe – automatisch, zonder enige code van jouw kant. display: none heeft dit nooit gedaan (Ctrl+F negeert het gewoon), en een gevirtualiseerde lijst kan dit niet doen, omdat de tekst waarnaar je zoekt simpelweg niet in de DOM zit totdat je er handmatig naartoe scrolt.

Praktisch voorbeeld: een reactielijst zonder één regel JS voor virtualisatie

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;
}

Het hele component rendert alle duizend <li>'s naar de DOM – geen logica voor een scrollvenster, geen berekening welke elementen toevallig zichtbaar zijn, geen library. De browser beslist op eigen houtje voor welke van de duizend reacties het de moeite waard is om op dit moment layout en paint te berekenen, en voor welke ze puur renderend gezien "niet bestaan", ook al bestaan ze in de DOM volledig.

Wanneer dit niet volstaat – en waarom virtualisatie nog steeds zin heeft

content-visibility lost de kost van renderen op – layout en paint. Het lost niet de kost op die in een React-applicatie vaak meer pijn doet: elk van de duizend <li>'s is nog steeds een echt React-component dat mount, hydrateert, en eigen hooks en effecten kan hebben. Het overslaan van CSS-rendering zorgt er niet voor dat die duizend componentinstanties ophouden te bestaan in de React-boom, en evenmin dat de JavaScript-kost om ze aan te maken verdwijnt. Voor eenvoudige, grotendeels statische inhoudsblokken (reacties, alinea's van een lang artikel, tabelrijen zonder interactiviteit) doet dat er niet toe – de echte kost lag toch al bij layout/paint, niet bij componentlogica. Voor lijsten van zware, interactieve componenten – elk met eigen state, subscripties, een dure render – blijft echte virtualisatie, die ongebruikte instanties volledig uit de React-boom verwijdert, soms de enige manier om het soepel te houden.

Valkuilen en goede praktijken

  • contain-intrinsic-size is in de praktijk verplicht, niet optioneel. Het weglaten ervan leidt tot een springende scroll en een verkeerde documenthoogte bij elke overgang van een element naar de overgeslagen modus en terug.
  • content-visibility: hidden is niet hetzelfde als auto. hidden slaat rendering altijd over, ongeacht de positie op het scherm, maar bewaart in tegenstelling tot display: none de interne staat van de berekende layout in een cache – een element opnieuw tonen (bijvoorbeeld bij het wisselen van tabblad) is goedkoper dan na display: none, omdat de browser niet alles vanaf nul hoeft te berekenen. Een element met content-visibility: hidden wordt daarbij correct verwijderd uit de accessibility-boom, net als display: none – een veilige keuze voor tabbladen en panelen die vaak worden gewisseld, niet alleen een performancetruc.
  • Dit is geen vervanging van virtualisatie voor lijsten met zware componenten. Beschouw het als het eerste, goedkopere instrument voor eenvoudige, grotendeels statische lijsten – grijp pas naar echte virtualisatie zodra een profiler daadwerkelijk kosten aan de JS/React-kant aanwijst, niet alleen aan de layoutkant.
  • Controleer de ondersteuning voordat je volledig op "hidden but matchable" vertrouwt. content-visibility: auto zelf heeft tegenwoordig brede ondersteuning in alle grote engines, maar het gedrag van "tijdelijk terugdraaien" bij Ctrl+F is vaak het meest uitgewerkt in Chromium – beschouw het als een leuke bonus, niet als een garantie waarop je een toegankelijkheidsvereiste baseert.

Samenvatting

Virtualisatie en content-visibility lossen op het eerste gezicht hetzelfde probleem op – een lange lijst die te traag rendert – maar doen dat op compleet verschillende niveaus. Virtualisatie verwijdert elementen volledig uit de DOM en uit de React-boom, ten koste van Ctrl+F, afdrukken en een deel van de toegankelijkheidsnavigatie. content-visibility laat de DOM volledig intact en laat de browser alleen het kostbare renderwerk overslaan voor wat toevallig onzichtbaar is – daarvoor krijg je een minder agressieve optimalisatie terug, maar wel gratis, zonder iets te breken dat de browser toch al kon met een volledig document. Voor de meeste lange, redelijk eenvoudige lijsten is dit het juiste eerste instrument – virtualisatie blijft achter de hand voor de gevallen waarin dit echt niet volstaat.