המורכבות של גלילה מתמשכת

רוברט פלאק
רוברט פלאק

אמ;לק: השתמשו שוב ברכיבי ה-DOM והסירו את הרכיבים שרחוקים משדה התצוגה. שימוש ב-placeholders כדי להביא בחשבון נתונים מעוכבים. לפניכם הדגמה והקוד של הגלילה האינסופית.

ברחבי האינטרנט צצות גלילה מתמשכת. רשימת האומנים ב-Google Music היא אחת, ציר הזמן של Facebook הוא אחד, וגם הפיד של Twitter בשידור חי. אתה גולל למטה ולפני שאתה מגיע לתחתית, תוכן חדש מופיע כבממה פתאום. מדובר בחוויה חלקה למשתמשים וקל לראות את מידת המשיכה.

עם זאת, האתגר הטכני שעומד מאחורי גלילה מתמשכת הוא קשה יותר ממה שנדמה. יש מגוון רחב של בעיות שנתקלתם בהן כשתרצו לבצע את The Right ThingTM. היא מתחילה בדברים פשוטים, כמו הקישורים בכותרת התחתונה שכמעט לא מגיעים אליהם, כי התוכן כל הזמן דוחף את הכותרת התחתונה. אבל הבעיות מורכבות יותר. איך מטפלים באירועים שמשנים את גודלם כאשר מישהו מסובב את הטלפון ממצב תצוגה לרוחב, או איך מונעים מהטלפון להיחרט והרשימה ארוכה מדי?

הדבר הנכוןTM

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

אנחנו נשתמש ב-3 שיטות כדי להשיג את המטרה שלנו: מיחזור DOM, מצבות ועגינת גלילה.

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

צילום מסך של אפליקציית Chat

מיחזור DOM

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

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

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

מסלול Sentinel

אותו עיקרון חל על גלילה בכיוון השני. עם זאת, לעולם לא נכווץ את המסלול בהטמעה שלנו, כדי שמיקום סרגל הגלילה יישאר עקבי.

פריטי Tombstone

כפי שהזכרנו קודם, אנחנו מנסים לגרום למקור הנתונים שלנו להתנהג כמו בעולם האמיתי. עם זמן אחזור של רשת והכל. המשמעות היא שאם המשתמשים שלנו משתמשים בגלילה מהירה, הם יכולים לגלול בקלות מעבר לרכיב האחרון שיש לנו נתונים לגביו. במקרה כזה, נציב פריט של מצבה – placeholder – יוחלף בפריט בתוכן אמיתי. המצבים גם ממוחזרים ויש להם מאגר נפרד לרכיבי DOM לשימוש חוזר. אנחנו צריכים לעשות זאת, כדי שנוכל לבצע מעבר נחמד ממצבה לפריט שאוכלס בתוכן. אחרת, היה מפריע מאוד למשתמש ולמעשה עלול לגרום לו לאבד את מה שהוא התכוון אליו.

קבר כזה. מאוד אבן. וואו.

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

עיגון גלילה

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

תרשים עיגון של גלילה.

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

פריסה

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

באופן אידאלי, פריטים ייצבעו מחדש פעם אחת בלבד לאחר שהם יחוברו ל-DOM, ולא יושפעו מהוספה או הסרה של פריטים אחרים במסלול. זה אפשרי, אבל רק בדפדפנים מודרניים.

שינויים מפתיעים

לאחרונה, נוספה ל-Chrome תמיכה ביצירת קונטיינרים של CSS – תכונה שמאפשרת למפתחים להגדיר לדפדפן שהרכיב מהווה גבול עבור פריסה ועבודת צביעה. אנחנו מתכננים כאן את הפריסה בעצמנו, לכן זו בקשה ראשית לבלימת ההדבקה. בכל פעם שאנחנו מוסיפים רכיב למסלול, אנחנו יודעים שהפריטים האחרים לא צריכים להיות מושפעים מהממסר. כך שכל פריט צריך להיות contain: layout. אנחנו גם לא רוצים להשפיע על שאר האתר שלנו, ולכן גם המסלול עצמו אמור לכלול את הוראת הסגנון הזו.

עוד דבר שהתייחסנו אליו הוא השימוש ב-IntersectionObservers כמנגנון לזיהוי מקרים שבהם המשתמש גלל מספיק רחוק כדי שנוכל להתחיל למחזר רכיבים ולטעון נתונים חדשים. עם זאת, שרת IntersectionObservers מוגדר לזמן אחזור גבוה (כמו בשימוש ב-requestIdleCallback), כך שלמעשה תרגיש פחות רספונסיביים עם שרתי IntersectionObservers מאשר בלי שרתי IntersectionOb. גם ההטמעה הנוכחית באמצעות האירוע scroll נתקלה בבעיה הזו, כי אירועי גלילה נשלחים על בסיס 'המאמץ הטוב ביותר'. בסופו של דבר, העבודה המורכבת של Houdini היא הפתרון באיכות גבוהה לבעיה זו.

זה עדיין לא מושלם

ההטמעה הנוכחית של מיחזור DOM אינה אידיאלית מכיוון שהיא מוסיפה את כל הרכיבים שעוברים דרך אזור התצוגה, במקום להתייחס רק לאלה שבאמת מופיעים במסך. המשמעות היא שכשאתם גוללים במהירות ממש, אתם משקיעים כל כך הרבה עבודת פריסה וציור ב-Chrome עד שהוא לא עומד בקצב. בסופו של דבר תראו רק את הרקע. זה לא סוף העולם אבל ללא ספק יש מקום לשיפור.

אנחנו מקווים שתגלו עד כמה בעיות פשוטות יכולות להיות מאתגרות, כשתרצו לשלב חוויית משתמש מעולה עם סטנדרטים גבוהים של ביצועים. מכיוון ש-Progressive Web Apps הופכות לחוויות ליבה בטלפונים ניידים, הדבר יהיה חשוב יותר ומפתחי האתרים יצטרכו להמשיך להשקיע בשימוש בדפוסים שמכבדים את אילוצי הביצועים.

כל הקוד מופיע במאגר שלנו. השקענו את מרב המאמצים כדי לאפשר שימוש חוזר בו, אבל לא נפרסם אותו כספרייה ב-npm או כמאגר נפרד. השימוש העיקרי הוא חינוכי.