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

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

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

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