הסטת פריסה מצטברת – CLS

הסטת פריסה מצטברת – CLS

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

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

מהו ציון CLS טוב?
כדי להעניק חוויית משתמש חיובית בעלי אתרים צריכים לשאוף לציון הנמוך מ-0.1. כדי לוודא שמצליחים להגיע לציון זה עבור מרבית המשתמשים, סף המדידה הנכון הוא האחוזון ה-75 בנתוני טעינת הדף, הן בסגמנט של משתמשי מכשירים ניידים והן בסגמנט של משתמשי מחשב.
מבט עומק בהסטות פריסה
הסטות פריסה מוגדרות באמצעות ה-API Layout Instability API (יציבות הפריסה), המתעדות ומדווחות על רשומות Layout-Shift בכל פעם שרכיב משנה את מיקומו בתצוגה בין שתי נקודות זמן. (למשל, שינוי של מיקומו ביחס לגבול העליון והשמאלי של המסך במיקום ברירת המחדל שלו בתצוגה)
חשוב לשים לב כי הסטות פריסה מתרחשות רק כאשר הרכיב משנה את מיקומו ההתחלתי. אם רכיב נוסף ל-DOM או שרכיב קיים משנה את גודלו, פעולות אלה אינן משוקללות כהסטת פריסה, כל עוד השינוי אינו גורם לרכיבים אחרים המוצגים על המסך לשנות את מיקומם ההתחלתי.
ציון הסטת הפריסה
כדי לחשב את ציון הסטת הפריסה, על הדפדפן לבחון את גודל התצוגה ואת התנועה של רכיבים לא יציבים בתצוגה זו בין שני פריימים שעובדו. ציון הסטת הפריסה הוא מכפלה של ערכים שונים המייצגים תנועה זו: "מרכיב ההשפעה" ו-"מרכיב המרחק" (שניהם מוגדרים בהמשך).
layout shift score = impact fraction * distance fraction
מרכיב ההשפעה

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

בתמונה מעל ישנו רכיב המתפרש על פני מחצית מהתצוגה בפריים אחד. לאחר מכן, בפריים הבא, הרכיב מוסט מטה בשיעור של 25% מגובה התצוגה. המלבן האדום המקווקו מציין את האיחוד בין שטחו הנראה לעין של הרכיב בשני הפריימים גם יחד. במקרה זה, מדובר ב-75% משטח התצוגה הכולל, לכן ערך רכיב ההשפעה יעמוד על 0.75.
מרכיב המרחק
החלק הנוסף במשוואה המרכיבה את ציון הסטת הפריסה הוא המרחק שהרכיב הלא־יציב נע ביחס לתצוגה כולה. מרכיב המרחק הוא המרחק הגדול ביותר שכל רכיב שאינו יציב נע בתוך הפריים (הן תנועה אנכית והן תנועה אופקית) המחולק לממד הגדול יותר של התצוגה (אורך או רוחב, הגדול מביניהם).

בדוגמה מעל הממד הגדול ביותר בתצוגה הוא האורך, והרכיב הלא יציב נע בשיעור של 25% של גובה התצוגה. לכן מרכיב המרחק יהיה 0.25.
לסיכום, בדוגמה שציינו מרכיב ההשפעה יהיה 0.75 ומרכיב המרחק יהיה 0.25, לכן ציון הסטת הפריסה יהיה: 0.1875=0.75*0.25
בתחילה, ציון הסטת הפריסה היה מחושב אך ורק על פי מרכיב ההשפעה. מרכיב המרחק נוסף למשוואה כדי לצמצם מקרים שבהם האתר נענש בחומרה בשל הסטות קלות של רכיבים גדולים על המסך.
הדוגמה הבאה ממחישה כיצד הוספה של תוכן לרכיב קיים משפיעה על ציון הסטת הפריסה:

כפתור "”Click Me! נוסף לתצוגה בתחתית הקובייה האפורה עם הטקסט השחור, פעולה זו דוחפת את הקובייה הירוקה עם הטקסט הלבן מטה (הוא נע באופן חלקי אל מחוץ לתצוגה).
בדוגמה זו, הקובייה הירוקה משנה את גודלה, אך נקודת ההתחלה שלה אינו משתנה, לכן במקרה זה לא מדובר ברכיב שאינו יציב.
הכפתור "Click Me!" לא היה קיים קודם לכן ב-DOM, לכן המיקום ההתחלתי שלו אף הוא נותר בעינו ואינו משתנה.
המיקום ההתחלתי של הקובייה הירושה, לעומת זאת, משתנה, אך מאחר והקובייה נעה באופן חלקי אל מחוץ לתצוגה, החלק הנראה לעין אינו נלקח בחשבון בחישוב מרכיב ההשפעה. איחוד השטח הנראה של הקובייה הירוקה בשני הפריימים (המיוצג באמצעות המלבן האדום המקווקו) הוא זהה לשטח של הקובייה הירוקה בפריים הראשון – 50% מהתצוגה. מרכיב ההשפעה הוא 0.5.
מרכיב המרחק מוצג באמצעות החץ הסגול. הקובייה הירוקה נעה בשיעור של 14% מהתצוגה, לכן ערך מרכיב המרחק הוא 0.14. ציון הסטת הפריסה הוא0.07 = 0.5 * 0.14
הדוגמה האחרונה מציגה מצב שבו קיימים שני רכיבים לא יציבים:

בתמונה מעלה, בפריים הראשון מוצגות ארבע תוצאות של בקשה מ-API לקבלת שמות בעלי חיים והצגתם לפי סדר אלף־בית. בפריים השני, נוספות תוצאות לרשימה הממויינת.
האיבר הראשון ברשימה ("חתול") אינו משנה את מיקומו ההתחלתי בין הפריימים, לכן הוא יציב. באותה מידה, האיברים הראשונים ברשימה לא היו נוכחים בתחילה ב-DOM, לכן המיקום הראשוני שלהם אף הוא אינו משתנה. אך, מיקומם של האיברים "כלב", "סוס" ו-"זברה" משתנה ולכן הם מוגדרים כרכיבים שאינם יציבים.
שוב המלבנים האדומים ממחישים את האיחוד בין שטחי הרכיבים הלא יציבים לפני ואחרי השינוי, במקרה זה השינוי הוא בשיעור של 38% משטח התצוגה הכולל. לכן מרכיב ההשפעה יעמוד על 0.38.
החצים מייצגים את המרחק שהרכיבים הלא יציבים נעו ממיקומם ההתחלתי. האיבר "זברה", המיוצג באמצעות החץ הכחול, נע בשיעור הרב ביותר, בשיעור של כ-30% מגובה התצוגה. לכן, מרכיב המרחק בדוגמה זו הוא 0.3.
ציון הסטת הפריסה הוא: 0.1172 = 0.38 * 0.3
הסטות פריסה צפויות מול הסטות פריסה שאינן צפויות
לא כל הסטת פריסה היא שלילית. למעשה, יישומוני רשת רבים משנים לעתים תכונות את מיקומם ההתחלתי של הרכיבים השונים בדף.
הסטת פריסה ביוזמת המשתמש
הסטת פריסה היא שלילית אם המשתמש לא ציפה לכך מראש. מצד שני, הסטות פריסה המתרחשות כתוצאה מאינטראקציה של המשתמש (הקלקה על לינק, לחיצה על כפתור, הקלקת ביטוי בתיבת חיפוש ופעולות נוספות שבוצעו על ידי המשתמש במכוון) הן בדרך כלל תקינות לחלוטין, כל עוד השינוי מתרחש קרוב ככל האפשר לפעולה שבוצעה, כדי שהקשר בין הפעולה לבין שינוי התצוגה יהיה נהיר למשתמש.
למשל, אם אינטראקציה מצד המשתמש מפעילה בקשת רשת העשויה לארוך זמן מה עד שתסתיים, עדיף להקצות שטח כלשהו בדף להצגת מחוון טעינה, זאת כדי להימנע משינוי פריסה מטריד כאשר התגובה לבקשה מסתיימת. אם המשתמש אינו מבין שהחלה טעינה של רכיב כלשהו, או שהוא לא יכול לחוש מתי התוכן שבקש לטעון יהיה מוכן, יתכן והוא ינסה להקליק על רכיב אחר בדף בעודו ממתין, אך רכיב זה עשוי להעלם פתאום מול עיניו המשתאות.
הסטות פריסה המתרחשות 500 מילי־שניות מהרגע שבו תועדה פעולת הזנת קלט אחרון לא יכללו במשוואה. הדבר מתבצע באמצעות סימון המאפיין  hadRecentInput.
הנפשות ומעברים
הנפשות ומעברים המבוצעים בצורה נכונה הם דרך נהדרת לעדכן את הדף מבלי להפתיע את המשתמש. תוכן המשתנה בפתאומיות בצורה בלתי צפויה בדף, ייצור כמעט תמיד חוויית משתמש שלילית. לעומת זאת, תכנים שנעים בהדרגה ובצורה טבעית ממצב אחד למשנהו, יסייעו לעתים קרובות לעזור למשתמשים להבין טוב יותר את המתרחש ויובילו  אותם בין מצב אחד לאחר.
מאפיין ה-CSS "Transform" מאפשר להנפיש רכיבים שונים בדף מבלי לגרום להסטות פריסה:
·      במקום לשנות את המאפיינים של גובה ורוחב (height, hidth), השתמשו בפונקציה transform: scale().
·      כדי להניע רכיבים ממקום למקום בתצוגה המנעו משינוי של המאפיינים:
top, right, bottom או left. במקום זאת השתמשו בפונקציה transform: translate().
כיצד למדוד CLS?
ניתן למדוד CLS בתנאי מעבדה או בתנאי שטח, ובדיקות אלה זמינות באמצעות הכלים הבאים:
זהירות: כלי מדידה בתנאי מעבדה טוענים את הדף בסביבה מלאכותית, ולכן יכולים למדוד הסטות פריסה המתרחשות במהלך טעינת הדף. לכן, ה-CLS המדווח על ידי כלי בדיקה אלה לדף נתון, עשויים להיות נמוכים מחוויית המשתמש של משתמשים אמיתיים בשטח.
מדידה בתנאי שטח
·      דו"ח חוויית המשתמש של כרום – Chrome User Experience Report
·      תובנות PageSpeed – PageSpeed Insights
·      דו"ח מדדי הליבה ברשת – Search Console (Core Web Vitals report)
מדידה בתנאי מעבדה
·      כלי הפיתוח של כרום – Chrome DevTools
·      פלטפורמת המגדלור – Lighthouse
·      אתר WebPageTest
מדידת CLS באמצעות ג'אווה סקריפט
הדרך הקלה ביותר למדוד CLS (כמו גם את כל מדדי הליבה ברשת) היא באמצעות ספריית הג'אווה סקריפט web-vitals, המאגדת את כל המורכבות הכרוכה במדידת ה-CLS לתוך פונקציה אחת:
import {getCLS} from 'web-vitals';
// Measure and log the current CLS value,
// any time it's ready to be reported.
getCLS(console.log);
כדי לחשב בצורה ידנית את ה-CLS, ניתן להשתמש ב-API שלLayout Instability . הדוגמה הבאה מציגה כיצד ניתן ליצור מופע של PerformanceObserver שיאזין לכל הרשומות של layout-shift, ויתעד אותן בקונסול:
// Use a try/catch instead of feature detecting `layout-shift`
// support, since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry);
}
});
po.observe({type: 'layout-shift', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
ה-CLS הוא סכום כל הרשומות הפרטניות שלא התרחשו במהלך קבלת הקלט האחרון מהמשתמש. כדי לחשב את ה-CLS יש להצהיר על משתנה שיאחסן את הציון, ולאחר מכן להוסיף אליו כל פעם שמתרחש שינוי פריסה בלתי צפוי.
במקום לדווח על כל שינוי של ה-CLS (העשוי להתרחש לעתים תכופות), מוטב לשמור את ערך ה-CLS הנוכחי ולדווח עליו בכל פעם שמצב מחזור החיים של הדף עובר ל"נסתר":
(להעתיק את בלוק הקוד הארוך כאן)
·       הערה:CrUX  מאגד את נתוני ה-CLS בקבוצות שכל אחת מהן מייצגת חמישה אחוזים, לכן אם התוצאה המתקבלת היא 0.01 היא תיכלל בקבוצה של 0%-5%, לעומת זאת אם התוצאה היא 0.07 היא תיכלל בקבוצה של 5%-10%.
כיצד ניתן לשפר את ה-CLS?
שיפור CLS במרבית האתרים יכול להתבצע באמצעות שמירה על מספר עקרונות מנחים:
·       נסו לכלול תמיד מאפייני גוגל בכל רכיבי התמונה והווידאו באתר, או שימרו את השטח הנדרש לרכיב באמצעות הגדרת יחסי גובה־רוחב בקובץ ה-CSS. באמצעות גישה זו תוכלו להבטיח כי הדפדפן יקצה את השטח המתאים למסמך בזמן שהתמונה נטענת. אפשרות נוספת היא שימוש במאפיין  Feature-Policy: unsized-media  בשורת ה-header  בקוד, מאפיין זה נתמך בחלק מהדפדפנים.
·       לעולם אל תשלבו תוכן חדש מעל תוכן קיים, מלבד במקרים של תגובה לאינטראקציה ביוזמת המשתמש. כך תוכלו להבטיח שהמשתמש יצפה לכל הסטות הפריסה.
·       העדיפו להשתמש בהנפשה מסוג Transform בכל פעם שאתם יוצרים הנפשה למאפיין הגורמים לשינויים בפריסת הדף. הנפישו את המעברים בצורה שתעניק הקשר למעבר ממצב אחד לשני.