צוואר הבקבוק של ה-DOM שאף אחד לא מדבר עליו
דמיינו לוח בקרה של שירות לקוחות שמושך עשרת אלפים רשומות לוג. או מערכת CRM שמנסה להציג כל איש קשר בטבלה אחת שניתן לגלול בה. ב-React, הקוד לבניית זה נראה בלתי מזיק מספיק. אתם עושים map על מערך, מחזירים קצת JSX, ומאפשרים ל-framework לעשות את עבודתו. הכל עובד מצוין בסביבת הפיתוח עם מאה שורות. ואז נכנסים נתוני הייצור (production), והדף הופך לבוץ.
הדפדפן לא מתעצל. הוא עושה בדיוק את מה שביקשתם, וזו הבעיה. כל שורה הופכת לצומת (node) ב-DOM. כל צומת מקבל עיצוב (style), פריסה (layout), צביעה (paint) ומעקב בזיכרון. כשאתם גוללים, הדפדפן מחשב מחדש מיקומים עבור כל העץ, לא רק עבור החלק שאתם מסתכלים עליו. מאזיני אירועים (event listeners) מצטברים. צריכת הזיכרון מזנקת. בסופו של דבר, ה-main thread נחנק זמן רב מספיק עד שהממשק מפסיק להגיב ללחיצות, הקשות או אפילו לגלילה עצמה. האפליקציה לא קרסה במובן הטכני, אבל עבור המשתמש שיושב מולה, החוויה שבורה באותה מידה.
זה קורה כי הדפדפן מנסה להחזיק כל אלמנט בודד בזיכרון פעיל בבת אחת. React עשויה להיות יעילה ביצירת תיאורים וירטואליים של ה-UI שלכם, אך ברגע שהתיאורים הללו הופכים לצמתים אמיתיים במסמך, הם עולים בדיוק כמו HTML שנכתב ידנית. אין "פתח מילוט" בתוך ה-framework עצמו. אתם זקוקים לשינוי מבני באופן שבו אתם מזינים את הרשימה ל-DOM.
מה המשמעות האמיתית של וירטואליזציה
וירטואליזציה היא השינוי המבני הזה. במקום לבקש מ-React לרנדר את כל המערך, אתם מרנדרים רק את הפריטים שיכולים להיכנס לתוך ה-viewport, בתוספת buffer קטן מעל ומתחת. ככל שהמשתמש גולל, האפליקציה משמיטה צמתים שיוצאים מהתצוגה ומייצרת חדשים שנכנסים מהקצה הנגדי. עבור המשתמש, זה עדיין מרגיש כמו רשימה רציפה אחת כי הגובה הכולל הניתן לגלילה נשמר, בדרך כלל באמצעות אלמנט מכולה (container) אחד גבוה או spacer שמחושב בקפידה. הפריטים הנראים הם פשוט חלון שמחליק לאורך מערך הנתונים.
חשבו על זה כמו סרט קולנוע שעובר דרך מנגנון המקרן. הקהל רואה תנועה חלקה, אך המכונה מאירה רק את הפריים שנמצא כרגע במיקום הנכון. שאר הסרט קיים בסלילי ההזנה והאיסוף, לא בנתיב האור. רשימות וירטואליות עובדות באותו אופן. מערך הנתונים הוא הסרט. ה-viewport הוא המנגנון.
זה לא lazy loading במובן המסורתי. lazy loading דוחה את שליפת הנתונים עד שהמשתמש גולל קרוב אליהם. וירטואליז
ראשית, הקונטיינר זקוק לגובה מוגדר. אם הרשימה נמצאת בתוך אלמנט אב שמתרחב כדי להתאים את ילדיו, וירטואליזציה לא יכולה לחשב אילו פריטים גלויים מכיוון שאין גבול viewport. עליך לקבע את הרשימה לגובה קבוע או לקונטיינר flex עם אילוצים ידועים.
שנית, לגודל הפריטים יש חשיבות עצומה. שורות בגובה קבוע הן המקרה הפשוט ביותר. הספרייה מכפילה את גובה השורה באינדקס ויודעת בדיוק היכן למקם כל אלמנט. תוכן בגובה משתנה, כמו הודעות צ'אט עם תמונות מוטמעות או שרשראות תגובות, מחייב את הספרייה למדוד לאחר ה-mount ולהתאים את עצמה תוך כדי תנועה. שלב המדידה הזה עלול לגרום ל-scroll jitter אם הוא קורה מאוחר מדי. אם הנתונים שלך מאפשרים זאת, כפה גבהים אחידים או גבהים מינימליים. אם לא, השתמש ב-variable-height virtualizer וקבל את המורכבות הנוספת.
שלישית, overscanning הוא חבר שלכם. רינדור של בדיוק מה שנכנס למסך יוצר רצועות לבנות ריקות כאשר המשתמש גולל במהירות. רוב הספריות מאפשרות לכם לרנדר כמה פריטים נוספים מעל ומתחת לאזור הגלוי. שתיים או שלוש שורות של overscan בדרך כלל מספיקות כדי להסתיר את התפרים מבלי לנפח שוב את ה-DOM.
רביעית, אל תתעלמו מה-key prop. ברשימה וירטואלית, פריטים משתמשים מחדש בצמתי DOM בזמן הגלילה. מפתחות (keys) יציבים מונעים מ-React לנחש לא נכון במהלך ה-reconciliation ולהרוס את ה-state בתוך רכיבי השורה. אם שורות הרשימה שלכם מכילות inputs, toggles או סקשנים מתרחבים, מפתחות לא תקינים יהרסו את מצב ה-UI בדרכים שנראות כמו באגים בשכבת הנתונים שלכם, אך הן למעשה טעויות רינדור.
מלכודת אחת עדינה היא ה-find-in-page של הדפדפן. מכיוון שהפריטים המוסתרים אינם קיימים ב-DOM, תיבת החיפוש של הדפדפן לא תראה אותם. אם המשתמשים שלכם מסתמכים על Ctrl+F כדי לאתר טקסט בתוך רשימה גדולה, תצטרכו לבנות חיפוש מותאם אישית שפועל מול סט הנתונים, ולא מול המסמך. גם קוראי מסך עלולים לאבד הקשר אם הסמנטיקה של הרשימה לא מטופלת בזהירות, לכן כדאי לבדוק עם טכנולוגיה מסייעת ולשקול הוספת הכרזות live region עבור טעינה דינמית.
מתי כדאי לדלג על זה
וירטואליזציה אינה בחינם. היא מוסיפה משקל של תלויות, חישובי קואורדינטות ועומס של אילוצים. אם הרשימה שלכם מגיעה לשיא של חמישים או מאה פריטים, הדפדפן יכול להתמודד עם זה ללא עזרה. רנדרו את הכל והמשיכו הלאה. הדבר נכון גם אם פריטי הרשימה שלכם מורכבים מאוד באופן אינדיבידואלי. וירטואליזציה חוסכת לכם אלפי צמתים, אך היא לא יכולה להציל אתכם מצומת אחד שמכיל תרשים ענק או אלמנט וידאו. תקנו קודם את ניפוח הפריטים.
הימנעו גם משימוש בווירטואליזציה כאשר הרשימה אינה גוללת. אם אתם משתמשים בדפדוף (pagination) עם כפתורי "הבא" ו"קודם" ומציגים רק עשרים פריטים בכל עמוד, אין מה לבצע לו windowing. הטכניקה משתלמת רק כאשר המשתמש מצפה לגלול לאורך רצף גדול ורציף.
השורה התחתונה
וירטואליזציה היא פחות בחירה בספרייה ויותר גישה (mindset). היא מאלצת אתכם להכיר בכך שה-DOM הוא משאב מוגבל, לא קנבס אינסופי. לפני שאתם מוסיפים אותה, פתחו את Chrome DevTools, הקליטו פרופיל ביצועים (performance profile), וודאו שזמן ה-layout או ה-paint הוא אכן האשם. ברגע שתדעו שה-DOM הוא צוואר הבקבוק, התחייבו לאילוצים. קבעו את הגבהים שלכם, שימו לב למפתחות, בצעו overscan מתון, ובדקו את הנגישות שלכם. כשעושים זאת נכון, רשימה וירטואלית הופכת "קיר נתונים" בלתי ניתן לשימוש למשהו שמרגיש קל כמו native scroll view. הדפדפן מפסיק להילחם, המשתמשים שלכם מפסיקים לחכות, והאפליקציה סוף סוף מתנהגת כמו הממשק המהיר שהתכוונתם לבנות.
