content-visibility: rendering offscreen uden listevirtualisering
content-visibility: rendering offscreen uden listevirtualisering
En liste med tusind kommentarer under en artikel renderer mærkbart langsomt, og scroll gennem den hakker på en svagere telefon. Standardsvaret i React er virtualisering – react-window eller @tanstack/react-virtual – som kun renderer de elementer i DOM'en, der lige nu befinder sig i det synlige vindue, plus en lille buffer. Det virker, men har en pris, der sjældent nævnes ved implementeringen: elementer uden for vinduet er ikke skjulte, de findes ikke i DOM'en. Et tryk på Ctrl+F finder ikke teksten i kommentar #850, før du selv har scrollet derhen. En sideudskrift viser kun det, der lige var renderet, da udskriften blev udløst. Skærmlæserens navigation via overskrifter eller landmarks springer indhold over, som fysisk ikke findes i træet – det er ikke en fejl ved virtualisering, det er dens uundgåelige konsekvens.
content-visibility angriber det samme ydelsesproblem fra en helt anden vinkel: elementerne bliver i DOM'en, altid, i deres helhed. Browseren springer bare det arbejde over, den ellers skulle udføre – layout, maling, oprettelse af kompositeringslag – indtil elementet kommer tæt nok på det synlige område til, at arbejdet giver mening.
Hvad "springer arbejde over" egentlig betyder
content-visibility: auto på et element beder browseren om at anvende CSS Containment på det – præcis den samme mekanisme, som i artiklen om Container Queries gjorde det muligt at undgå selvsving ved forespørgsel på containerens størrelse. Her tjener containment et andet formål: hvis et element er offscreen, kan browseren trygt antage, at dets interne layout og maling ikke påvirker noget uden for det selv, så den kan udskyde det arbejde helt, uden at genberegne en eneste pixel indeni.
css
.comment {content-visibility: auto;contain-intrinsic-size: auto 180px;}
contain-intrinsic-size er ikke en kosmetisk detalje – uden den ville et oversprunget element krympe til nul højde, fordi browseren ikke har noget sted at hente dets størrelse fra, når den ikke beregner layoutet indeni. Det er præcis det samme problem som med container-type: size uden eksplicit højde – bare her er konsekvensen ikke et forsvindende kort, men en hoppende scrollbar, fordi dokumentets højde pludselig ændrer sig, hver gang endnu en kommentar går ind i eller ud af den oversprungne tilstand. Værdien auto 180px fortæller browseren to ting på én gang: brug 180px som første tilnærmelse, før elementet overhovedet er blevet renderet en eneste gang, og husk derefter dets faktiske, beregnede størrelse, og brug den huskede værdi som placeholder ved hver efterfølgende overspringning – så hvis de reelle kommentarer har forskellig højde, gætter browseren over tid mere og mere præcist, hvor meget plads der skal reserveres, i stedet for stift at holde fast i ét enkelt tal.
Et trick, du ikke får ved fuld virtualisering: "hidden until found"
Her gør content-visibility noget, virtualisering slet ikke kan gøre – ikke bare i praksis, men principielt. Et element i auto-tilstand har, selvom det er oversprunget renderingsmæssigt, stadig sin tekst fysisk i DOM'en. Browseren ved dette og behandler et sådant element som "skjult, men søgbart" (hidden but matchable): når brugeren trykker Ctrl+F og indtaster en sætning, der matcher teksten inde i det oversprungne element, fortryder browseren midlertidigt overspringningen, renderer det for alvor, fremhæver træffet og scroller til det – automatisk, uden en linje kode fra din side. display: none har aldrig gjort det (Ctrl+F ignorerer det simpelthen), og en virtualiseret liste kan ikke gøre det, fordi teksten, du søger efter, simpelthen ikke findes i DOM'en, før du selv har scrollet derhen.
Praktisk eksempel: en kommentarliste uden en eneste linje JS til virtualisering
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;}
Hele komponenten renderer alle tusind <li>-elementer til DOM'en – ingen logik for scroll-vindue, ingen beregning af, hvilke elementer der lige nu er synlige, intet bibliotek. Browseren afgør på egen hånd, for hvilke af de tusind kommentarer det er værd at beregne layout og maling i det givne øjeblik, og for hvilke de renderingsmæssigt "ikke eksisterer", selvom de findes i deres helhed i DOM'en.
Når det ikke er nok – og hvorfor virtualisering stadig giver mening
content-visibility løser omkostningen ved rendering – layout og maling. Den løser ikke den omkostning, der ofte gør mere ondt i en React-app: hver af de tusind <li>-elementer er stadig en reel React-komponent, som mountes, hydreres og kan have sine egne hooks og effekter. At springe CSS-rendering over gør ikke, at de tusind komponentinstanser holder op med at eksistere i React-træet, eller at den JavaScript, der kræves for at oprette dem, holder op med at koste noget. For simple, overvejende statiske indholdsblokke (kommentarer, afsnit i en lang artikel, tabelrækker uden interaktivitet) er det uden betydning – den reelle omkostning lå alligevel i layout/maling, ikke i komponentlogikken. For lister bestående af tunge, interaktive komponenter – hver med egen state, subscriptions, dyr rendering – kan reel virtualisering, som fjerner ubrugte instanser fra React-træet helt, stadig være den eneste måde at bevare flydende ydelse på.
Faldgruber og god praksis
contain-intrinsic-sizeer obligatorisk i praksis, ikke valgfri. Udelades den, fører det til hoppende scroll og forkert dokumenthøjde, hver gang et element skifter til oversprunget tilstand og tilbage igen.content-visibility: hiddener ikke det samme somauto.hiddenspringer rendering over altid, uanset position på skærmen, men i modsætning tildisplay: nonebevarer den den interne, beregnede layout-tilstand i en cache – at vise elementet igen (fx ved skift af fane) er billigere end efterdisplay: none, fordi browseren ikke beregner alt fra bunden. Et element medcontent-visibility: hiddenfjernes samtidig korrekt fra tilgængelighedstræet, ligesomdisplay: none– det er et sikkert valg til ofte skiftede faner og paneler, ikke kun et ydelsestrick.- Det er ikke en erstatning for virtualisering ved lister med tunge komponenter. Betragt det som det første, billigere værktøj til simple, overvejende statiske lister – grib først til reel virtualisering, når profileren faktisk peger på en omkostning i JS/React, ikke bare i layoutet.
- Tjek understøttelsen, før du læner dig helt op ad "hidden but matchable".
content-visibility: autoi sig selv har i dag bred understøttelse i alle store motorer, men adfærden med "midlertidig afsløring" ved Ctrl+F er ofte mest gennemarbejdet i Chromium – betragt det som en rar bonus, ikke som en garanti, du baserer et tilgængelighedskrav på.
Opsummering
Virtualisering og content-visibility løser ved første øjekast det samme problem – en lang liste, der renderer for langsomt – men de gør det på helt forskellige niveauer. Virtualisering fjerner elementer fra DOM'en og fra React-træet fuldstændigt, og betaler for det med tabet af Ctrl+F, udskrift og dele af den tilgængelighedsrelaterede navigation. content-visibility lader DOM'en forblive intakt og beder browseren om kun at springe det dyre renderingsarbejde over for det, der lige nu er usynligt – til gengæld får du en mindre aggressiv optimering, men gratis, uden at bryde noget, browseren i forvejen kunne gøre med et fuldt dokument. For de fleste lange, nogenlunde simple lister er det det rette første værktøj – virtualisering står i reserve til de tilfælde, hvor det reelt ikke er nok.