בקצרה: אפשר לעשות שימוש חוזר ברכיבי ה-DOM ולהסיר את הרכיבים שמרוחקים מאזור התצוגה. משתמשים ב-placeholder כדי להסביר את העיכוב בנתונים. כאן אפשר לראות הדגמה של גלילה אינסופית, וכאן אפשר לראות את הקוד.
גלילה מתמשכת מופיעה בכל מקום באינטרנט. רשימת האומנים ב-Google Music היא אחת, ציר הזמן בפייסבוק הוא אחד והפיד בטוויטר הוא אחד. אתם גוללים למטה ולפני שמגיעים לתחתית, תוכן חדש מופיע באופן פתאומי, כאילו משום מקום. המשתמשים נהנים מחוויה חלקה וקלה.
עם זאת, האתגר הטכני שמאחורי גלילה אינסופית הוא מורכב יותר ממה שנראה. הבעיות שאתם עלולים להיתקל בהן כשאתם רוצים לעשות את הדבר הנכון™ הן רבות. זה מתחיל בדברים פשוטים כמו הקישורים בכותרת התחתונה, שהופכים לבלתי נגישים כי התוכן דוחף את הכותרת התחתונה למטה. אבל הבעיות הופכות לקשות יותר. איך מטפלים באירוע של שינוי גודל כשמישהו מסובב את הטלפון ממצב לאורך למצב לרוחב, או איך מונעים מהטלפון להיתקע בצורה מעצבנת כשהרשימה ארוכה מדי?
The right thing™
חשבנו שזו סיבה מספיק טובה ליצור הטמעה לדוגמה שמראה דרך לטפל בכל הבעיות האלה באופן שניתן לשימוש חוזר, תוך שמירה על סטנדרטים של ביצועים.
אנחנו נשתמש ב-3 טכניקות כדי להשיג את המטרה שלנו: שימוש חוזר ב-DOM, מצבות וקיבוע גלילה.
הדוגמה שלנו תהיה חלון צ'אט כמו ב-Hangouts, שבו אפשר לגלול בין ההודעות. קודם כל, אנחנו צריכים מקור אינסופי של הודעות בצ'אט. מבחינה טכנית, אף אחד מהגלילות האינסופיות לא באמת אינסופי, אבל כמות הנתונים שזמינה להזנה לגלילות האלה היא כל כך גדולה, שאפשר לומר שהן אינסופיות. לצורך הפשטות, נבצע קידוד קשיח של קבוצת הודעות בצ'אט ונבחר הודעה, מחבר וצירוף תמונה מדי פעם באופן אקראי, עם השהיה מלאכותית כדי שההתנהגות תהיה דומה יותר לרשת האמיתית.

מיחזור DOM
מיחזור DOM היא טכניקה שלא נעשה בה מספיק שימוש, שמטרתה לשמור על מספר נמוך של צומתי DOM. הרעיון הכללי הוא להשתמש ברכיבי DOM שכבר נוצרו ולא מוצגים במסך, במקום ליצור רכיבים חדשים. אמנם צומתי DOM עצמם לא יקרים, אבל הם לא בחינם, כי כל אחד מהם מוסיף עלות נוספת בזיכרון, בפריסה, בסגנון ובציור. אם לאתר יש DOM גדול מדי לניהול, הביצועים של מכשירים ברמה נמוכה יהיו איטיים באופן משמעותי, או שהאתר לא יהיה שמיש בכלל. חשוב לזכור גם שכל פריסה מחדש וכל החלה מחדש של הסגנונות – תהליך שמופעל בכל פעם שמוסיפים או מסירים מחלקה מצומת – הופכות ליקרות יותר ככל ש-DOM גדול יותר. השימוש החוזר בצמתי ה-DOM מאפשר לנו לשמור על מספר נמוך בהרבה של צמתי ה-DOM, וכך לקצר את כל התהליכים האלה.
המכשול הראשון הוא הגלילה עצמה. מכיוון שבכל רגע נתון יהיה לנו רק חלק קטן מכל הפריטים שזמינים ב-DOM, אנחנו צריכים למצוא דרך אחרת כדי שפס הגלילה בדפדפן ישקף בצורה נכונה את כמות התוכן שקיימת באופן תיאורטי. אנחנו נשתמש באלמנט sentinel בגודל 1px על 1px עם טרנספורמציה כדי לאלץ את האלמנט שמכיל את הפריטים – המסלול – להיות בגובה הרצוי. אנחנו נעביר כל רכיב במסלול ההמראה לשכבה משלו כדי לוודא שהשכבה של מסלול ההמראה עצמה ריקה לחלוטין. ללא צבע רקע, ללא רקע. אם השכבה של מסלול ההמראה לא ריקה, היא לא עומדת בדרישות לאופטימיזציות של הדפדפן, ונצטרך לאחסן טקסטורה בכרטיס המסך שלנו בגובה של כמה מאות אלפי פיקסלים. בטח שלא במכשיר נייד.
בכל פעם שמגללים, המערכת בודקת אם אזור התצוגה התקרב מספיק לסוף המסלול. אם כן, נאריך את ה-runway על ידי הזזת רכיב הסנטינל, הזזת הפריטים שיצאו מאזור התצוגה לחלק התחתון של ה-runway ואכלוס שלהם בתוכן חדש.
אותו דבר קורה כשגוללים לכיוון השני. עם זאת, אנחנו אף פעם לא נקצר את משך הזמן שמוקצב להטמעה, כדי שמיקום סרגל הגלילה יישאר עקבי.
פריטי Tombstone
כמו שציינו קודם, אנחנו מנסים לגרום למקור הנתונים שלנו להתנהג כמו משהו בעולם האמיתי. כולל זמן האחזור של הרשת. כלומר, אם המשתמשים שלנו משתמשים בגלילה מהירה, הם יכולים בקלות לדלג על הרכיב האחרון שיש לנו נתונים לגביו. במקרה כזה, נציב פריט מצבה – placeholder – שיוחלף בפריט עם התוכן בפועל כשהנתונים יגיעו. גם מצבות ממוחזרות ויש להן מאגר נפרד של רכיבי DOM שאפשר לעשות בהם שימוש חוזר. אנחנו צריכים את זה כדי ליצור מעבר חלק מסימן קבורה לפריט שאוכלס בתוכן. אחרת, המעבר יהיה בולט מדי למשתמש, ועלול לגרום לו לאבד את המיקוד.

אתגר מעניין כאן הוא שגובה הפריטים האמיתיים יכול להיות גדול יותר מגובה פריט המצבה, בגלל כמויות שונות של טקסט בכל פריט או בגלל תמונה שמצורפת. כדי לפתור את הבעיה, נשנה את מיקום הגלילה הנוכחי בכל פעם שמתקבלים נתונים ומתבצעת החלפה של מצבה מעל אזור התצוגה. נבצע עיגון של מיקום הגלילה לרכיב במקום לערך פיקסל. המושג הזה נקרא עיגון גלילה.
הצמדה לגלילה
ההצמדה לגלילה תופעל גם כשמצבות קבורה מוחלפות וגם כשגודל החלון משתנה (מה שקורה גם כשמכשירים מסובבים!). נצטרך לגלות מהו הרכיב הכי עליון שגלוי באזור התצוגה. יכול להיות שהרכיב הזה גלוי רק באופן חלקי, ולכן נשמור גם את ההיסט מהחלק העליון של הרכיב שבו מתחיל אזור התצוגה.

אם משנים את גודל אזור התצוגה ויש שינויים בנתיב ההמראה, אנחנו יכולים לשחזר מצב שנראה זהה למשתמש. ניצחת! אבל אם משנים את גודל החלון, יכול להיות שגובה כל אחד מהפריטים ישתנה. אז איך יודעים כמה רחוק למטה צריך למקם את התוכן המעוגן? אנחנו לא! כדי לגלות את הגובה של הפריט המעוגן, נצטרך להציג כל רכיב שמעליו ולהוסיף את כל הגבהים שלהם. פעולה כזו עלולה לגרום להשהיה משמעותית אחרי שינוי הגודל, ואנחנו לא רוצים שזה יקרה. במקום זאת, אנחנו מניחים שכל פריט שלמעלה הוא באותו גודל כמו מצבה, ומשנים את מיקום הגלילה בהתאם. כשהאלמנטים נגללים אל המסלול, אנחנו משנים את מיקום הגלילה, ובפועל דוחים את עבודת הפריסה עד שהיא נדרשת.
פריסה
דילגתי על פרט חשוב: פריסה. בדרך כלל, כל מיחזור של רכיב DOM יגרום לפריסה מחדש של כל המסלול, וכך נגיע הרבה מתחת ליעד של 60 פריימים לשנייה. כדי להימנע מכך, אנחנו לוקחים על עצמנו את האחריות לפריסה ומשתמשים ברכיבים עם מיקום מוחלט וטרנספורמציות. כך אנחנו יכולים להעמיד פנים שכל הרכיבים שבהמשך המסלול עדיין תופסים מקום, כשבפועל יש רק מקום ריק. מכיוון שאנחנו מבצעים את הפריסה בעצמנו, אנחנו יכולים לשמור במטמון את המיקומים שבהם כל פריט מופיע, ויכולים לטעון באופן מיידי את הרכיב הנכון מהמטמון כשהמשתמש גולל אחורה.
באופן אידיאלי, פריטים צריכים להיצבע מחדש רק פעם אחת כשהם מצורפים ל-DOM, ולא להיות מושפעים מהוספה או מהסרה של פריטים אחרים ב-runway. אפשר לעשות את זה, אבל רק בדפדפנים מודרניים.
שינויים מתקדמים
לאחרונה, ב-Chrome נוספה תמיכה ב-CSS Containment, תכונה
שמאפשרת לנו, המפתחים, להגדיר לדפדפן שרכיב מסוים הוא גבול לעבודת הפריסה והציור. מכיוון שאנחנו יוצרים את הפריסה בעצמנו, זהו מקרה שימוש מצוין לשימוש בתכונה containment. בכל פעם שאנחנו מוסיפים רכיב למסלול, אנחנו יודעים שאין צורך לשנות את הפריסה של הפריטים האחרים. לכן כל פריט צריך לקבל את הערך contain: layout
. אנחנו גם לא רוצים להשפיע על שאר האתר, לכן גם המסלול עצמו צריך לקבל את הוראת הסגנון הזו.
שיקול נוסף היה שימוש ב-IntersectionObservers
כמנגנון לזיהוי המצב שבו המשתמש גלל מספיק רחוק כדי שנוכל להתחיל למחזר רכיבים ולטעון נתונים חדשים. עם זאת, מוגדר ש-IntersectionObservers הם בעלי חביון גבוה (כמו שימוש ב-requestIdleCallback
), ולכן יכול להיות ש-IntersectionObservers יגרמו לתחושה של תגובה פחות מהירה מאשר בלי להשתמש בהם. גם ההטמעה הנוכחית שלנו באמצעות האירוע scroll
סובלת מהבעיה הזו, כי אירועי גלילה נשלחים על בסיס 'המאמץ הטוב ביותר'. בסופו של דבר, Houdini’s Compositor Worklet יהיה הפתרון לבעיה הזו עם רמת דיוק גבוהה.
הוא עדיין לא מושלם
ההטמעה הנוכחית שלנו של שימוש חוזר ב-DOM לא אידיאלית, כי היא מוסיפה את כל הרכיבים שעוברים דרך אזור התצוגה, במקום להתייחס רק לרכיבים שמוצגים במסך. כלומר, כשגוללים מהר מאוד, Chrome צריך לעבוד קשה מאוד כדי להציג את הפריסה והצבעים, ולפעמים הוא לא מצליח לעמוד בקצב. בסופו של דבר, לא תראו כלום חוץ מהרקע. זה לא סוף העולם, אבל בהחלט יש מקום לשיפור.
אנחנו מקווים שהבנתם כמה בעיות פשוטות יכולות להיות מאתגרות כשרוצים לשלב בין חוויית משתמש מעולה לבין סטנדרטים גבוהים של ביצועים. אפליקציות אינטרנט מתקדמות הופכות לחלק מרכזי בחוויית השימוש בטלפונים ניידים, ולכן הנושא הזה יהפוך לחשוב יותר ומפתחי אתרים יצטרכו להמשיך להשקיע בשימוש בדפוסים שמביאים בחשבון את מגבלות הביצועים.
אפשר למצוא את כל הקוד במאגר שלנו. עשינו כמיטב יכולתנו כדי שיהיה אפשר להשתמש בו שוב, אבל לא נפרסם אותו כספרייה בפועל ב-npm או כמאגר נפרד. השימוש העיקרי הוא למטרות חינוכיות.