content-visibility: renderowanie offscreen bez wirtualizacji listy
content-visibility: renderowanie offscreen bez wirtualizacji listy
Lista tysiąca komentarzy pod artykułem renderuje się zauważalnie wolno, a scroll przez nią zacina się na słabszym telefonie. Standardowa odpowiedź w Reakcie to wirtualizacja – react-window albo @tanstack/react-virtual – renderującą w DOM tylko te elementy, które akurat mieszczą się w widocznym oknie, plus niewielki bufor. Działa, ale ma koszt, o którym rzadko mówi się przy wdrażaniu: elementy poza oknem nie są ukryte, one nie istnieją w DOM. Wciśnięcie Ctrl+F nie znajdzie tekstu w komentarzu #850, dopóki nie przewiniesz do niego ręcznie. Wydruk strony pokaże tylko to, co akurat było renderowane w momencie wywołania druku. Nawigacja czytnika ekranu po nagłówkach czy landmarkach pomija treść, która fizycznie nie istnieje w drzewie – to nie jest błąd wirtualizacji, to jej nieunikniona konsekwencja.
content-visibility atakuje ten sam problem wydajnościowy z zupełnie innej strony: elementy zostają w DOM, zawsze, w komplecie. Przeglądarka po prostu pomija dla nich pracę, którą normalnie musiałaby wykonać – layout, malowanie, tworzenie warstw kompozytorskich – dopóki element nie znajdzie się wystarczająco blisko widocznego obszaru, żeby ta praca miała sens.
Co dokładnie znaczy „pomija pracę”
content-visibility: auto na elemencie każe przeglądarce zastosować do niego CSS Containment – dokładnie ten sam mechanizm, który w artykule o Container Queries pozwalał uniknąć zapętlenia przy odpytywaniu rozmiaru kontenera. Tutaj containment służy czemu innemu: jeśli element jest offscreen, przeglądarka może bezpiecznie założyć, że jego wewnętrzny layout i malowanie nie wpływają na nic poza nim samym, więc może je odłożyć w całości, bez przeliczania ani jednego piksela w środku.
css
.comment {content-visibility: auto;contain-intrinsic-size: auto 180px;}
contain-intrinsic-size nie jest kosmetycznym dodatkiem – bez niego pominięty element skurczyłby się do zera wysokości, bo przeglądarka nie ma skąd wziąć jego rozmiaru, skoro nie liczy layoutu w środku. To dokładnie ten sam problem, co przy container-type: size bez jawnej wysokości – tylko tutaj konsekwencją nie jest znikająca karta, tylko skaczący pasek przewijania, bo wysokość dokumentu nagle się zmienia za każdym razem, gdy kolejny komentarz wchodzi lub wychodzi z trybu pominiętego. Wartość auto 180px mówi przeglądarce dwie rzeczy naraz: użyj 180px jako pierwszego przybliżenia, zanim element zostanie choć raz naprawdę wyrenderowany, a potem zapamiętaj jego rzeczywisty, wyliczony rozmiar i używaj tej zapamiętanej wartości jako placeholdera przy każdym kolejnym pominięciu – więc jeśli realne komentarze mają różną wysokość, przeglądarka z czasem coraz trafniej zgaduje, ile miejsca zarezerwować, zamiast sztywno trzymać się jednej liczby.
Sztuczka, której nie dostaniesz przy pełnej wirtualizacji: hidden until found
To miejsce, w którym content-visibility robi coś, czego virtualizacja zrobić nie może w zasadzie – nie tylko w praktyce. Element w trybie auto, mimo że pominięty renderowo, wciąż ma swój tekst fizycznie w DOM. Przeglądarka wie o tym i traktuje taki element jako „ukryty, ale przeszukiwalny” (hidden but matchable): kiedy użytkownik wciska Ctrl+F i wpisuje frazę, która pasuje do tekstu wewnątrz pominiętego elementu, przeglądarka tymczasowo cofa pominięcie, renderuje go naprawdę, podświetla dopasowanie i przewija do niego – automatycznie, bez żadnego kodu z Twojej strony. display: none nigdy tego nie robił (Ctrl+F go zwyczajnie ignoruje), a zwirtualizowana lista nie może tego zrobić, bo tekstu, którego szukasz, po prostu nie ma w DOM, dopóki nie przewiniesz tam ręcznie.
Praktyczny przykład: lista komentarzy bez jednej linijki JS do wirtualizacji
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;}
Cały komponent renderuje wszystkie tysiąc <li> do DOM – żadnej logiki okna przewijania, żadnego przeliczania, które elementy akurat są widoczne, żadnej biblioteki. Przeglądarka na własną rękę decyduje, dla których z tysiąca komentarzy warto policzyć layout i malowanie w danej chwili, a dla których czysto renderowo „nie istnieją”, mimo że w DOM istnieją w komplecie.
Kiedy to nie wystarcza – i dlaczego wirtualizacja wciąż ma sens
content-visibility rozwiązuje koszt renderowania – layoutu i malowania. Nie rozwiązuje kosztu, który w aplikacji React często boli bardziej: każdy z tysiąca <li> to wciąż realny komponent Reacta, który się montuje, hydratuje, może mieć własne hooki i efekty. Pominięcie renderowania CSS-owego nie sprawia, że te tysiąc instancji komponentów przestaje istnieć w drzewie Reacta ani że przestaje kosztować JavaScript potrzebny do ich utworzenia. Dla prostych, w większości statycznych bloków treści (komentarze, akapity długiego artykułu, wiersze tabeli bez interaktywności) to nieistotne – prawdziwy koszt i tak leżał w layout/paint, nie w logice komponentu. Dla list złożonych z ciężkich, interaktywnych komponentów – każdy z własnym stanem, subskrypcjami, kosztownym renderem – realna wirtualizacja, usuwająca nieużywane instancje z drzewa Reacta całkowicie, wciąż bywa jedynym sposobem na utrzymanie płynności.
Pułapki i dobre praktyki
contain-intrinsic-sizejest obowiązkowe w praktyce, nie opcjonalne. Pominięcie go prowadzi do skaczącego scrolla i błędnej wysokości dokumentu przy każdym przejściu elementu w tryb pominięty i z powrotem.content-visibility: hiddento nie to samo coauto.hiddenpomija renderowanie zawsze, niezależnie od pozycji na ekranie, ale w przeciwieństwie dodisplay: nonezachowuje wewnętrzny stan wyliczonego layoutu w pamięci podręcznej – ponowne pokazanie elementu (np. przełączenie zakładki) jest tańsze niż podisplay: none, bo przeglądarka nie liczy wszystkiego od zera. Element zcontent-visibility: hiddenjest przy tym poprawnie usuwany z drzewa dostępności, tak jakdisplay: none– to bezpieczny wybór do zakładek i paneli przełączanych często, nie tylko sztuczka wydajnościowa.- To nie substytut wirtualizacji dla list z ciężkimi komponentami. Traktuj je jako pierwsze, tańsze narzędzie do prostych, w większości statycznych list – po realną wirtualizację sięgaj dopiero, gdy profiler faktycznie wskaże koszt po stronie JS/Reacta, nie samego layoutu.
- Sprawdź wsparcie przed pełnym poleganiem na
hidden but matchable. Samocontent-visibility: automa dziś szerokie wsparcie we wszystkich głównych silnikach, ale zachowanie „tymczasowego odsłonięcia” przy Ctrl+F bywa najbardziej dopracowane w Chromium – potraktuj je jako miły bonus, nie jako gwarancję, na której opierasz wymaganie dostępności.
Podsumowanie
Wirtualizacja i content-visibility rozwiązują na pierwszy rzut oka ten sam problem – długą listę, która renderuje się zbyt wolno – ale robią to na zupełnie innych piętrach. Wirtualizacja usuwa elementy z DOM i z drzewa Reacta całkowicie, płacąc za to utratą Ctrl+F, druku i części nawigacji dostępnościowej. content-visibility zostawia DOM w komplecie i każe przeglądarce pominąć tylko kosztowną pracę renderowania dla tego, co akurat niewidoczne – w zamian dostajesz mniej agresywną optymalizację, ale za darmo, bez łamania niczego, co przeglądarka i tak już potrafiła robić z pełnym dokumentem. Dla większości długich, w miarę prostych list to właściwe pierwsze narzędzie – wirtualizacja zostaje w rezerwie na przypadki, w których to realnie nie wystarczy.