מניעת שיבושים עקב ביצועי רינדור טובים יותר

Tom Wiltzius
Tom Wiltzius

מבוא

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

זהו המאמר הראשון בסדרה של מאמרים שעוסקים באופטימיזציה של ביצועי העיבוד בדפדפן. בתור התחלה, נסביר למה קשה ליצור אנימציה חלקה ומה צריך לעשות כדי להשיג אותה, וגם בכמה שיטות מומלצות. רבים מהרעיונות האלה הוצגו במקור ב "Jank Busters", הרצאה שנאט דוקה והעברתי השנה בהרצאת Google I/O (סרטון).

חדש: V-sync

יכול להיות ששחקני מחשב מכירים את המונח הזה, אבל זה נדיר באינטרנט: מה זה v-sync?

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

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

התזמון הוא הכול: requestAnimationFrame

מפתחי אתרים רבים משתמשים ב-setInterval או ב-setTimeout כל 16 אלפיות השנייה כדי ליצור אנימציות. זוהי בעיה מסיבות שונות (ואנו נדון בהרחבה בעוד דקה), אבל החששות העיקריים הם:

  • רזולוציית הטיימר מ-JavaScript מוצגת רק בסדר גודל של כמה אלפיות השנייה
  • למכשירים שונים יש קצב רענון שונה

זכור את הבעיה בתזמון הפריימים שהוזכרה למעלה: כדי להיות מוכן לפני הרענון הבא של המסך, אתה צריך מסגרת אנימציה מלאה, שסיימת עם JavaScript, מניפולציית DOM, פריסה, ציור וכו'. רזולוציית טיימר נמוכה עשויה להקשות על השלמת פריימים של אנימציה לפני הרענון הבא של המסך, אבל שינויים בקצב הרענון של המסך לא מאפשרים להשתמש בטיימר קבוע. לא משנה מה מרווח הזמן של הטיימר, תיסחפו לאט מחלון התזמון של פריים מסוים ובסוף תעזבו את חלון הזמנים. זה יקרה גם אם הטיימר מופעל בדיוק של אלפית שנייה, והוא לא (כפי שהמפתחים גילו) – רזולוציית הטיימר משתנה בהתאם לסוללה של המכשיר ולחשמל, או אם היא מחוברת לחשמל, עשויה להיות מושפעת מכרטיסיות רקע שמגייסות משאבים וכו'. גם אם המצב הזה נדיר (למשל, כל 16 פריימים כי חלפו אלפית שנייה), ניתן להבחין במספר פריימים. תבוצע גם עבודה על יצירת פריימים שלא מוצגים אף פעם, דבר שמבזבז את צריכת החשמל וזמן המעבד (CPU) שיכול להיות שאתם משקיעים בביצוע פעולות אחרות באפליקציה שלכם.

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

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

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

ל-requestAnimationFrame יש גם תכונות נחמדות נוספות:

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

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

תקציב של מסגרת

אנחנו רוצים שמסגרת חדשה תהיה מוכנה בכל רענון מסך, ולכן נותר רק הזמן בין הרענוןים לבצע את כל העבודה ליצירת פריים חדש. בתצוגת של 60Hz, המשמעות היא שיש לנו בערך 16 אלפיות השנייה להריץ את כל ה-JavaScript, לבצע פריסה, לצבוע וכל דבר אחר שהדפדפן צריך לעשות כדי להוציא את המסגרת. כלומר, אם הרצת ה-JavaScript בתוך הקריאה החוזרת (callback) של requestAnimationFrame נמשכת יותר מ-16 אלפיות השנייה, אין לך תקווה ליצור מסגרת בזמן ל-v-sync!

16 אלפיות השנייה זה לא הרבה זמן. למרבה המזל, הכלים למפתחים של Chrome יכולים לעזור לכם לזהות מקרים שבהם אתם מנצלים את תקציב הפריים שלכם במהלך הקריאה החוזרת של requestAnimationFrame.

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

הדגמה עם יותר מדי פריסה
הדגמה עם יותר מדי פריסה

הקריאות החוזרות של requestAnimationFrame (rAF) האלה נמשכות יותר מ-200 אלפיות השנייה. סדר הגודל הזה ארוך מדי בשביל לסמן פריים בכל 16 אלפיות שנייה! פתיחת אחת מהקריאות החוזרות הארוכות האלה של rAF חושפת מה מתרחש בפנים: במקרה הזה, הרבה פריסה.

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

הדגמה מעודכנת עם פריסה מצומצמת יותר
הדגמה מעודכנת עם פריסה מצומצמת יותר

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

מקור אחר של יאנק

הגורם העיקרי לבעיה בהפעלת אנימציות שמבוססות על JavaScript הוא שדברים אחרים יכולים להפריע לקריאה החוזרת (callback) של rAF, ואפילו למנוע את ההפעלה שלה. גם אם הקריאה החוזרת (callback) של ה-rAF נמוכה ופועלת תוך כמה אלפיות שנייה, פעילויות אחרות (כמו עיבוד XHR שנכנס, הפעלת handlers של אירועי קלט או הרצת עדכונים מתוזמנים בטיימר) יכולות להגיע פתאום ולפעול למשך זמן מסוים ללא היקפו. במכשירים ניידים, לפעמים, עיבוד האירועים האלה יכול להימשך מאות אלפיות שנייה, ובמשך הזמן הזה האנימציה תפסיק לפעול. אנחנו מכנים את התאמות האנימציה jank.

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

  • אל תבצעו יותר מדי עיבוד ברכיבי handler של קלט! פעולות רבות של JS או ניסיון לארגן מחדש את כל הדף, למשל, handler של הפעלה גלילה הם סיבה שכיחה מאוד לחוסר תקינות נוראית.
  • מומלץ להטמיע כמה שיותר עיבוד (קריאה: כל מה שייקח הרבה זמן) בקריאה החוזרת (callback) של ה-rAF או ב-Web Workers.
  • אם אתם דוחפים את העבודה בקריאה החוזרת של ה-rAF, נסו לחלק אותה כך שעיבודו יתבצע רק מעט כל פריים, או שתעכבו אותה עד שמסתיימת אנימציה חשובה. כך תוכלו להמשיך להפעיל קריאות חוזרות (callback) קצרות של rAF וליצור אנימציה בצורה חלקה.

לעיון במדריך מצוין כיצד לדחוף עיבוד אל קריאות חוזרות של requestAnimationFrame במקום אל רכיבי handler של קלט, ראו את המאמר של פול לואיס Leaner, Meaner, Faster Animations with requestAnimationFrame.

הנפשת CSS

מה טוב יותר מ-JS קליל באירוע שלך ומקריאות חוזרות של rAF? אין JS.

קודם לכן אמרנו שאין פתרון קסם שימנע הפרעות בקריאות החוזרות (callback) של rAF, אבל אתם יכולים להשתמש באנימציית CSS כדי להימנע לחלוטין מהצורך בהן. ב-Chrome ל-Android במיוחד (ודפדפנים אחרים פועלים עם תכונות דומות), לאנימציות CSS יש את המאפיין הרצוי ביותר שבדרך כלל הדפדפן יכול להפעיל אותן גם אם JavaScript פועל.

בקטע שלמעלה יש הצהרה משתמעת לגבי jank: דפדפנים יכולים לעשות רק דבר אחד בכל פעם. זה לא נכון לחלוטין, אבל זו הנחה מועילה: בכל רגע נתון הדפדפן יכול להפעיל JS, לבצע פריסה או לצייר, אבל רק אחד בכל פעם. אפשר לבדוק את זה בתצוגת ציר הזמן בכלי הפיתוח. אחד היוצאים מן הכלל לכלל הזה הוא אנימציות CSS ב-Chrome ל-Android (ובקרוב ב-Chrome למחשב, אבל עדיין לא).

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

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

אם לוחצים על הלחצן, JavaScript פועל במשך 180 אלפיות השנייה וגורם לבעיות בממשק (jank). אבל אם במקום זאת ננהג את האנימציה באמצעות אנימציות CSS, ה-jank לא יופיע יותר.

(חשוב לזכור שבזמן כתיבה, אנימציית CSS היא ללא בעיות בממשק Chrome ל-Android בלבד, ולא ב-Chrome במחשב.)

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

למידע נוסף על השימוש באנימציות של CSS, אפשר לעיין במאמרים כמו המאמר הזה ב-MDN.

סיכום

בקצרה:

  1. כשיוצרים אנימציה, חשוב ליצור פריימים לכל רענון מסך. לאנימציה של Vsync יש השפעה חיובית מאוד על התחושה באפליקציה.
  2. הדרך הטובה ביותר ליצור אנימציה Vsyncd ב-Chrome ובדפדפנים מודרניים אחרים היא להשתמש באנימציית CSS. אם אתם זקוקים לגמישות רבה יותר מזו שמסופקת על ידי אנימציית CSS, השיטה הטובה ביותר היא לבקש אנימציה המבוססת על AnimationFrame.
  3. כדי שאנימציות של rAF יהיו בריאות ושמחות, חשוב לוודא שמטפלי אירועים אחרים לא יפריעו לפעולה של קריאה חוזרת (callback) של rAF, ולדאוג שקריאות חוזרות (callback) של rAF יהיו קצרות (פחות מ-15 אלפיות השנייה).

לסיום, אנימציית vsync'd לא חלה רק על אנימציות פשוטות של ממשק המשתמש, היא חלה על אנימציית Canvas2D, אנימציית WebGL ואפילו גלילה בדפים סטטיים. במאמר הבא בסדרה זו נתעמק בביצועי הגלילה מתוך מחשבה על המושגים האלה.

אנימציית אנימציה שמחה!

קובצי עזר