content-visibility: rendering offscreen utan listvirtualisering
En lista med tusen kommentarer under en artikel renderas märkbart långsamt, och scrollningen genom den hackar på en svagare telefon. Standardsvaret i React är virtualisering – react-window eller @tanstack/react-virtual – som bara renderar de element i DOM som faktiskt ryms i det synliga fönstret, plus en liten buffert. Det fungerar, men har en kostnad som sällan nämns vid införandet: elementen utanför fönstret är inte dolda, de existerar inte i DOM. Att trycka Ctrl+F hittar inte texten i kommentar #850 förrän du manuellt scrollat till den. Sidutskrift visar bara det som råkade vara renderat i det ögonblick utskriften anropades. Skärmläsarens navigering via rubriker eller landmärken hoppar över innehåll som fysiskt inte finns i trädet – det är inget fel hos virtualiseringen, det är dess oundvikliga konsekvens.
content-visibility angriper samma prestandaproblem från ett helt annat håll: elementen finns kvar i DOM, alltid, i sin helhet. Webbläsaren hoppar bara över det arbete den normalt skulle behöva utföra – layout, målning, skapande av kompositeringslager – tills elementet hamnar tillräckligt nära det synliga området för att arbetet ska vara meningsfullt.
Vad "hoppar över arbete" egentligen betyder
content-visibility: auto på ett element tvingar webbläsaren att tillämpa CSS Containment på det – exakt samma mekanism som i artikeln om Container Queries lät oss undvika en loop vid förfrågningar om containerns storlek. Här tjänar containment ett annat syfte: om ett element är offscreen kan webbläsaren tryggt anta att dess interna layout och målning inte påverkar något utanför det själv, så den kan skjuta upp allt det arbetet helt, utan att räkna om en enda pixel inuti.
css
.comment {content-visibility: auto;contain-intrinsic-size: auto 180px;}
contain-intrinsic-size är inget kosmetiskt tillägg – utan det skulle ett överhoppat element krympa till nollhöjd, eftersom webbläsaren inte har någonstans att hämta dess storlek ifrån när den inte beräknar layouten inuti. Det är exakt samma problem som med container-type: size utan explicit höjd – bara att konsekvensen här inte är ett försvinnande kort, utan en hoppande scrollbar, eftersom dokumentets höjd plötsligt ändras varje gång ännu en kommentar går in i eller ut ur det överhoppade läget. Värdet auto 180px säger två saker till webbläsaren samtidigt: använd 180px som första uppskattning innan elementet ens har renderats en enda gång på riktigt, och kom sedan ihåg dess faktiska, uträknade storlek och använd det ihågkomna värdet som platshållare vid varje efterföljande överhoppning – så om de riktiga kommentarerna har olika höjd gissar webbläsaren med tiden allt bättre hur mycket plats som ska reserveras, i stället för att envist hålla fast vid en enda siffra.
Tricket du inte får med full virtualisering: "hidden until found"
Det är här content-visibility gör något som virtualisering i princip inte kan göra – inte bara i praktiken. Ett element i auto-läge, trots att det är överhoppat renderingsmässigt, har fortfarande sin text fysiskt kvar i DOM. Webbläsaren vet om det och behandlar ett sådant element som "dolt men sökbart" (hidden but matchable): när användaren trycker Ctrl+F och skriver en fras som matchar text inuti det överhoppade elementet, återställer webbläsaren tillfälligt överhoppningen, renderar elementet på riktigt, markerar träffen och scrollar dit – automatiskt, utan en rad kod från din sida. display: none har aldrig gjort det (Ctrl+F ignorerar det helt enkelt), och en virtualiserad lista kan inte göra det, eftersom texten du söker efter helt enkelt inte finns i DOM förrän du manuellt scrollat dit.
Praktiskt exempel: en kommentarslista utan en rad JS för 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;}
Hela komponenten renderar alla tusen <li> till DOM – ingen logik för scrollfönster, ingen uträkning av vilka element som råkar vara synliga, inget bibliotek. Webbläsaren avgör själv för vilka av de tusen kommentarerna det är värt att beräkna layout och målning just nu, och för vilka de rent renderingsmässigt "inte existerar", trots att de finns kvar i DOM i sin helhet.
När det inte räcker – och varför virtualisering fortfarande har sin plats
content-visibility löser kostnaden för rendering – layout och målning. Den löser inte kostnaden som ofta gör mest ont i en React-app: var och en av de tusen <li> är fortfarande en riktig React-komponent som monteras, hydreras, och som kan ha egna hooks och effekter. Att hoppa över CSS-rendering gör inte att de tusen komponentinstanserna slutar existera i React-trädet, eller att den JavaScript som krävs för att skapa dem slutar kosta något. För enkla, till stor del statiska innehållsblock (kommentarer, stycken i en lång artikel, tabellrader utan interaktivitet) spelar det ingen roll – den verkliga kostnaden låg ändå i layout/paint, inte i komponentlogiken. För listor byggda av tunga, interaktiva komponenter – var och en med eget state, prenumerationer, kostsam rendering – är riktig virtualisering, som helt tar bort oanvända instanser ur React-trädet, fortfarande ibland det enda sättet att hålla scrollningen smidig.
Fallgropar och god praxis
contain-intrinsic-sizeär obligatoriskt i praktiken, inte valfritt. Utelämnar du det får du en hoppande scrollbar och en felaktig dokumenthöjd varje gång ett element växlar in i och ut ur det överhoppade läget.content-visibility: hiddenär inte samma sak somauto.hiddenhoppar över rendering alltid, oavsett position på skärmen, men till skillnad fråndisplay: nonebehåller den det interna, uträknade layout-tillståndet i en cache – att visa elementet igen (t.ex. vid flikbyte) är billigare än efterdisplay: none, eftersom webbläsaren inte behöver räkna om allt från början. Ett element medcontent-visibility: hiddentas dessutom korrekt bort ur tillgänglighetsträdet, precis somdisplay: none– ett säkert val för flikar och paneler som växlas ofta, inte bara ett prestandatrick.- Det ersätter inte virtualisering för listor med tunga komponenter. Betrakta det som det första, billigare verktyget för enkla, till stor del statiska listor – ta till riktig virtualisering först när profileraren faktiskt visar en kostnad på JS/React-sidan, inte bara i layouten.
- Kontrollera stödet innan du helt förlitar dig på "hidden but matchable".
content-visibility: autoi sig har idag brett stöd i alla stora motorer, men beteendet "tillfällig avslöjning" vid Ctrl+F är ofta mest utpolerat i Chromium – behandla det som en trevlig bonus, inte som en garanti att bygga ett tillgänglighetskrav på.
Sammanfattning
Virtualisering och content-visibility löser vid första anblicken samma problem – en lång lista som renderas för långsamt – men gör det på helt olika nivåer. Virtualisering tar bort element ur DOM och ur React-trädet helt, till priset av förlorad Ctrl+F, utskrift och delar av tillgänglighetsnavigeringen. content-visibility lämnar DOM intakt och låter webbläsaren bara hoppa över det kostsamma renderingsarbetet för det som just nu är osynligt – i gengäld får du en mindre aggressiv optimering, men gratis, utan att bryta något som webbläsaren redan kunde göra med ett fullständigt dokument. För de flesta långa, någorlunda enkla listor är det rätt förstahandsverktyg – virtualisering finns kvar i reserv för de fall där det verkligen inte räcker.