content-visibility: רינדור מחוץ למסך בלי וירטואליזציית רשימה
content-visibility: רינדור מחוץ למסך בלי וירטואליזציית רשימה
רשימה של אלף תגובות מתחת למאמר מתרנדרת לאט בצורה מורגשת, והגלילה בה נתקעת בטלפון חלש יותר. התשובה הסטנדרטית ב-React היא וירטואליזציה – react-window או @tanstack/react-virtual – שמרנדרים ב-DOM רק את האלמנטים שנמצאים כרגע בחלון הנראה, בתוספת מאגר קטן. זה עובד, אבל יש לזה מחיר שכמעט אף פעם לא מוזכר בזמן היישום: אלמנטים מחוץ לחלון לא מוסתרים, הם לא קיימים ב-DOM. לחיצה על Ctrl+F לא תמצא טקסט בתגובה #850, עד שתגללו אליה ידנית. הדפסת העמוד תציג רק את מה שהיה מרונדר בדיוק ברגע קריאת ההדפסה. ניווט של קורא מסך לפי כותרות או landmarks מדלג על תוכן שלא קיים פיזית בעץ – זו לא תקלה בוירטואליזציה, זו תוצאה בלתי נמנעת שלה.
content-visibility תוקף את אותה בעיית ביצועים מזווית שונה לגמרי: האלמנטים נשארים ב-DOM, תמיד, בשלמותם. הדפדפן פשוט מדלג עבורם על העבודה שהוא בדרך כלל היה צריך לבצע – layout, ציור, יצירת שכבות קומפוזיציה – עד שהאלמנט מגיע קרוב מספיק לאזור הנראה כדי שהעבודה הזו תהיה בעלת משמעות.
מה בדיוק פירוש "מדלג על עבודה"
content-visibility: auto על אלמנט מכריח את הדפדפן להחיל עליו CSS Containment – בדיוק אותו מנגנון שבמאמר על Container Queries אפשר להימנע מלולאה כשמשאלים על גודל המיכל. כאן ה-containment משרת מטרה אחרת: אם האלמנט נמצא מחוץ למסך, הדפדפן יכול להניח בבטחה שה-layout והציור הפנימיים שלו לא משפיעים על שום דבר מחוץ לו עצמו, ולכן יכול לדחות אותם כליל, בלי לחשב אפילו פיקסל אחד בפנים.
css
.comment {content-visibility: auto;contain-intrinsic-size: auto 180px;}
contain-intrinsic-size הוא לא תוספת קוסמטית – בלעדיו, אלמנט שדולג היה מתכווץ לגובה אפס, כי לדפדפן אין מאיפה לקחת את הגודל שלו, מכיוון שהוא לא מחשב layout בפנים. זו בדיוק אותה בעיה כמו container-type: size בלי גובה מפורש – רק שכאן התוצאה היא לא כרטיסייה נעלמת, אלא פס גלילה קופצני, כי גובה המסמך משתנה פתאום בכל פעם שתגובה נוספת נכנסת או יוצאת ממצב דילוג. הערך auto 180px אומר לדפדפן שני דברים בבת אחת: השתמש ב-180px כקירוב ראשוני, לפני שהאלמנט אי פעם באמת יתרנדר, ואז זכור את הגודל האמיתי, המחושב שלו, והשתמש בערך הזכור הזה כ-placeholder בכל דילוג עתידי – אז אם לתגובות האמיתיות יש גבהים שונים, הדפדפן עם הזמן מנחש בדיוק הולך וגובר כמה מקום להקצות, במקום להיצמד בקשיחות למספר אחד.
טריק שלא תקבלו בוירטואליזציה מלאה: hidden until found
זה המקום שבו content-visibility עושה משהו שוירטואליזציה לא יכולה לעשות בעיקרון – לא רק בפועל. אלמנט במצב auto, למרות שדולג רינדורית, עדיין מכיל את הטקסט שלו פיזית ב-DOM. הדפדפן יודע זאת ומתייחס לאלמנט כזה כ"מוסתר אבל ניתן להתאמה" (hidden but matchable): כשמשתמש לוחץ Ctrl+F ומקליד ביטוי שתואם לטקסט בתוך אלמנט שדולג, הדפדפן מבטל את הדילוג זמנית, מרנדר אותו באמת, מדגיש את ההתאמה וגולל אליה – אוטומטית, בלי שום קוד מצדכם. display: none מעולם לא עשה את זה (Ctrl+F פשוט מתעלם ממנו), ורשימה מווירטואלת לא יכולה לעשות את זה, כי הטקסט שאתם מחפשים פשוט לא קיים ב-DOM, עד שתגללו אליו ידנית.
דוגמה מעשית: רשימת תגובות בלי שורת JS אחת לוירטואליזציה
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;}
הרכיב כולו מרנדר את כל אלף ה-<li> ל-DOM – בלי שום לוגיקת חלון גלילה, בלי שום חישוב של אילו אלמנטים כרגע נראים, בלי שום ספרייה. הדפדפן מחליט בעצמו עבור אילו מתוך אלף התגובות שווה לחשב layout וציור ברגע נתון, ועבור אילו הם, מבחינה רינדורית טהורה, "לא קיימים", למרות שב-DOM הם קיימים במלואם.
מתי זה לא מספיק – ולמה וירטואליזציה עדיין הגיונית
content-visibility פותר את עלות הרינדור – layout וציור. הוא לא פותר את העלות שבאפליקציית React לעיתים קרובות כואבת יותר: כל אחד מאלף ה-<li> הוא עדיין רכיב React אמיתי, שמתחבר (mount), עובר הידרציה, ואולי יש לו hooks ו-effects משלו. דילוג על הרינדור ב-CSS לא גורם לאלף המופעים האלה של הרכיב להפסיק להתקיים בעץ של React, ולא גורם ל-JavaScript הדרוש ליצירתם להפסיק לעלות. עבור בלוקים פשוטים, בעיקר סטטיים (תגובות, פסקאות של מאמר ארוך, שורות טבלה בלי אינטראקטיביות), זה לא משנה – העלות האמיתית ממילא הייתה ב-layout/paint, לא בלוגיקת הרכיב. עבור רשימות מורכבות של רכיבים כבדים ואינטראקטיביים – כל אחד עם state, subscriptions, רינדור יקר משלו – וירטואליזציה אמיתית, שמסירה מופעים לא בשימוש מעץ ה-React לגמרי, עדיין לפעמים הדרך היחידה לשמור על חלקות.
מלכודות ושיטות עבודה טובות
contain-intrinsic-sizeהכרחי בפועל, לא אופציונלי. דילוג עליו מוביל לגלילה קופצנית ולגובה מסמך שגוי בכל מעבר של אלמנט למצב דילוג וחזרה ממנו.content-visibility: hiddenזה לא אותו דבר כמוauto.hiddenמדלג על הרינדור תמיד, בלי קשר למיקום על המסך, אבל בניגוד ל-display: noneשומר את המצב הפנימי של ה-layout המחושב במטמון – הצגה חוזרת של האלמנט (למשל מעבר בין טאבים) זולה יותר מאשר אחריdisplay: none, כי הדפדפן לא מחשב הכול מאפס. אלמנט עםcontent-visibility: hiddenמוסר בצורה תקינה מעץ הנגישות, בדיוק כמוdisplay: none– זו בחירה בטוחה לטאבים ופאנלים שמוחלפים לעיתים קרובות, לא רק טריק ביצועים.- זה לא תחליף לוירטואליזציה ברשימות עם רכיבים כבדים. התייחסו אליו ככלי ראשון וזול לרשימות פשוטות, בעיקר סטטיות – פנו לוירטואליזציה אמיתית רק כשהפרופיילר מראה בפועל עלות בצד ה-JS/React, לא רק ב-layout.
- בדקו תמיכה לפני הסתמכות מלאה על
hidden but matchable. ל-content-visibility: autoעצמו יש כיום תמיכה רחבה בכל המנועים המרכזיים, אבל ההתנהגות של "חשיפה זמנית" ב-Ctrl+F נוטה להיות המהוקצעת ביותר ב-Chromium – התייחסו אליה כבונוס נחמד, לא כערובה שעליה מבססים דרישת נגישות.
סיכום
וירטואליזציה ו-content-visibility פותרות במבט ראשון את אותה בעיה – רשימה ארוכה שמתרנדרת לאט מדי – אבל עושות זאת בקומות שונות לגמרי. וירטואליזציה מסירה אלמנטים מה-DOM ומעץ ה-React לגמרי, ומשלמת על כך באובדן Ctrl+F, הדפסה וחלק מהניווט הנגיש. content-visibility משאיר את ה-DOM שלם ומכריח את הדפדפן לדלג רק על עבודת הרינדור היקרה עבור מה שכרגע לא נראה – בתמורה מקבלים אופטימיזציה פחות אגרסיבית, אבל בחינם, בלי לשבור שום דבר שהדפדפן ממילא כבר ידע לעשות עם המסמך המלא. עבור רוב הרשימות הארוכות והפשוטות יחסית, זה הכלי הראשון הנכון – וירטואליזציה נשארת ברזרבה למקרים שבהם זה באמת לא מספיק.