Parallaxin'

מבוא

אתרי Parallax פופולריים מאוד לאחרונה. כדאי לכם להעיף מבט באתרים הבאים:

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

דף הדגמה עם אפקט פרלקס
דף ההדגמה שלנו עם אפקט פרלקס

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

אפשר להכליל אתר עם אפקט פרלקס כך:

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

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

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

אפשרות 1: שימוש ברכיבי DOM ובמיקומים מוחלטים

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

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

כלי הפיתוח ל-Chrome בלי אירועי גלילה עם דחייה (debounce).
כלי הפיתוח שמוצגים בו פריטים גדולים שצוירו על המסך ומספר פריסות שמופעלות על ידי אירועים בפריים אחד.

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

נזיז את קוד העדכון מאירוע הגלילה ל-callback של requestAnimationFrame, ונשתמש ב-callback של אירוע הגלילה כדי לתעד את ערך הגלילה.

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

כלי הפיתוח ל-Chrome עם אירועי גלילה עם דחייה (debounce).
כלי הפיתוח שמוצגים בו פריטים גדולים שצוירו על המסך ומספר פריסות שמופעלות על ידי אירועים בפריים אחד.

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

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

אפשרות 2: שימוש ברכיבי DOM ובטרנספורמציות תלת-ממדיות

במקום להשתמש במיקומים מוחלטים, אפשר להחיל על הרכיבים טרנספורמציות תלת-ממדיות. במצב כזה, אנחנו רואים שהרכיבים עם הטרנספורמציות התלת-ממדיות שמופעלות עליהם מקבלים שכבה חדשה לכל רכיב, ובדפדפני WebKit, לרוב זה גורם גם למעבר למעבד הגרפי (hardware compositor). לעומת זאת, באפשרות 1 הייתה שכבה אחת גדולה של הדף, שצריך היה לצייר מחדש בכל פעם שהיה שינוי, וכל תהליך הציור והקומפוזיציה בוצע על ידי המעבד.

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

לפעמים אנשים פשוט משתמשים בהאק של -webkit-transform: translateZ(0); ורואים שיפורים קסומים בביצועים. אמנם השיטה הזו עובדת היום, אבל יש בה בעיות:

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

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

לבסוף, כדאי להימנע משימוש בצבעים ככל האפשר ולהזיז פשוט את הרכיבים הקיימים בדף. לדוגמה, גישה אופיינית לאתרים עם אפקט תלת-ממד היא להשתמש ב-divs בגובה קבוע ולשנות את מיקום הרקע שלהם כדי ליצור את האפקט. לצערנו, המשמעות היא שצריך לצייר מחדש את הרכיב בכל חזרה, וזה עלול להשפיע לרעה על הביצועים. במקום זאת, אם אפשר, כדאי ליצור את האלמנט (לעטוף אותו בתוך div עם overflow: hidden אם צריך) ולתרגם אותו במקום זאת.

אפשרות 3: שימוש ב-canvas במיקום קבוע או ב-WebGL

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

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

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


/**
 * Updates and draws in the underlying visual elements to the canvas.
 */
function updateElements () {

  var relativeY = lastScrollY / h;

  // Fill the canvas up
  context.fillStyle = "#1e2124";
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Draw the background
  context.drawImage(bg, 0, pos(0, -3600, relativeY, 0));

  // Draw each of the blobs in turn
  context.drawImage(blob1, 484, pos(254, -4400, relativeY, 0));
  context.drawImage(blob2, 84, pos(954, -5400, relativeY, 0));
  context.drawImage(blob3, 584, pos(1054, -3900, relativeY, 0));
  context.drawImage(blob4, 44, pos(1400, -6900, relativeY, 0));
  context.drawImage(blob5, -40, pos(1730, -5900, relativeY, 0));
  context.drawImage(blob6, 325, pos(2860, -7900, relativeY, 0));
  context.drawImage(blob7, 725, pos(2550, -4900, relativeY, 0));
  context.drawImage(blob8, 570, pos(2300, -3700, relativeY, 0));
  context.drawImage(blob9, 640, pos(3700, -9000, relativeY, 0));

  // Allow another rAF call to be scheduled
  ticking = false;
}

/**
 * Calculates a relative disposition given the page's scroll
 * range normalized from 0 to 1
 * @param {number} base The starting value.
 * @param {number} range The amount of pixels it can move.
 * @param {number} relY The normalized scroll value.
 * @param {number} offset A base normalized value from which to start the scroll behavior.
 * @returns {number} The updated position value.
 */
function pos(base, range, relY, offset) {
  return base + limit(0, 1, relY - offset) * range;
}

/**
 * Clamps a number to a range.
 * @param {number} min The minimum value.
 * @param {number} max The maximum value.
 * @param {number} value The value to limit.
 * @returns {number} The clamped value.
 */
function limit(min, max, value) {
  return Math.max(min, Math.min(max, value));
}

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

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

יכול להיות שהתגובה המיידית שלכם תהיה ש-WebGL הוא מוגזם או שהוא לא נתמך בכל מקום, אבל אם אתם משתמשים במשהו כמו Three.js, תמיד תוכלו לחזור להשתמש באלמנט בד והקוד שלכם יהיה מופשט באופן עקבי ונוח. כל מה שצריך לעשות הוא להשתמש ב-Modernizr כדי לבדוק אם יש תמיכה בממשק ה-API המתאים:

// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
  renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
  renderer = new THREE.CanvasRenderer();
}

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

הבחירה היא שלך

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

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

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

סיכום

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

וכמו תמיד, לא משנה באיזו גישה תבחרו: אל תנחשו, בדקו.