פרלקסציה ביצועית

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

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

איור של Parallax.

אמ;לק

  • אין להשתמש באירועי גלילה או ב-background-position כדי ליצור אנימציות פרלקס.
  • השתמשו בהמרות CSS 3D כדי ליצור אפקט פרלקס מדויק יותר.
  • ב-Safari לנייד, יש להשתמש ב-position: sticky כדי להבטיח שאפקט הפרלקס יופץ.

אם אתם רוצים את הפתרון החדשני, תוכלו לעבור אל מאגר UI Element Samples GitHub ולהשתמש ב-Parallax helper JS! במאגר של GitHub תוכלו לראות הדגמה בזמן אמת של פס הגלילה בפרלקס.

פרלקס בעיות

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

לא טוב: שימוש באירועי גלילה

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

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

לא טוב: מתבצע עדכון של background-position

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

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

CSS בתלת ממד

סקוט קלום וקית' קלארק עשו עבודה משמעותית בתחום של השימוש ב-CSS 3D כדי ליצור תנועת פרלקס, והשיטה שבה הם משתמשים היא למעשה:

  • מגדירים רכיב מכיל כדי לגלול עם overflow-y: scroll (וככל הנראה, overflow-x: hidden).
  • על אותו רכיב להחיל ערך perspective, ו-perspective-origin שמוגדר כ-top left, או 0 0.
  • לילדי הרכיב הזה מחילים תרגום ב-Z ומגדילים את גודלו כדי לספק תנועת פרלקס בלי להשפיע על הגודל שלהם על המסך.

ה-CSS בגישה הזו נראה כך:

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

בהנחה שיש קטע HTML כזה:

<div class="container">
    <div class="parallax-child"></div>
</div>

התאמת קנה המידה לפרספקטיבה

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

במקרה של הקוד שלמעלה, הפרספקטיבה היא 1px ומרחק ה-Z של parallax-child הוא -2px. כלומר, צריך להתאים את גודל הרכיב ל-3x, ואפשר לראות את הערך שמחובר לקוד: scale(3).

בכל תוכן שלא הוחל עליו ערך translateZ, אפשר להמיר ערך של אפס. כלומר, הסולם הוא (פרספקטיבה - 0) / פרספקטיבה, שהערך שלו גדל ב-1. כלומר, קנה המידה לא הוגדל או לא עולה. די שימושי, באמת.

איך הגישה הזו עובדת

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

עם זאת, החלת ערך נקודת מבט על רכיב הגלילה מבלבלת את התהליך הזה, כי היא משנה את המטריצות שעליהן מבוסס הטרנספורמציה של הגלילה. עכשיו, כשגלילה של 300px יכולה להזיז את הילדים רק ב-150px, בהתאם לערכים של perspective ושל translateZ שבחרתם. אם לרכיב יש ערך translateZ של 0, תתבצע גלילה ביחס של 1:1 (כמו בעבר), אבל צאצאים שנדחפו ב-Z ממקור הפרספקטיבה ייגללו בקצב שונה! התוצאה הסופית: תנועת פרלקס. נוסף לכך, חשוב מאוד לטפל באופן אוטומטי כחלק ממכונות הגלילה הפנימית של הדפדפן, כך שאין צורך להאזין לאירועים של scroll או לשנות את background-position.

זבוב למשחה: Mobile Safari

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

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

ב-HTML שלמעלה, .parallax-container הוא חדש, ולמעשה הוא ישטח את הערך perspective ונאבד את אפקט הפרלקס. ברוב המקרים, הפתרון די פשוט: מוסיפים transform-style: preserve-3d לרכיב, וכך מפיצים את האפקטים התלת-ממדיים (כמו ערך הפרספקטיבה שלנו) שהוחלו במעלה העץ.

.parallax-container {
  transform-style: preserve-3d;
}

במקרה של Mobile Safari, לעומת זאת, הדברים קצת יותר מורכבים. מבחינה טכנית, ההחלה של overflow-y: scroll על רכיב הקונטיינר פועלת, אבל במחיר היכולת להניע את רכיב הגלילה. הפתרון הוא להוסיף -webkit-overflow-scrolling: touch, אבל זה גם ישטח את perspective ולא נקבל פרלקסיה.

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

position: sticky באים להצלה!

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

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

על ידי החלת position: -webkit-sticky על רכיב הפרלקס, אפשר להפוך בפועל את אפקט השטוח של -webkit-overflow-scrolling: touch. כך אפשר לוודא שהרכיב בפרלקס מפנה לאב הקדמון הקרוב ביותר באמצעות תיבת גלילה, שבמקרה הזה היא .container. לאחר מכן, בדומה לאפשרות הקודמת, הפונקציה .parallax-container מיישמת את הערך perspective, שמשנה את היסט הגלילה המחושב ויוצר אפקט פרלקס.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

כך משחזרים את אפקט הפרלקס ב-Mobile Safari, וזו חדשות מצוינות לכל אורך!

אזהרות לגבי מיקום דביק

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

  • עם position: sticky, ככל שהרכיב קרוב יותר ל-z=0, פחות הוא זז.
  • בלי position: sticky, ככל שהרכיב קרוב יותר ל-z=0, כך יותר הוא זז.

אם כל זה נראה מופשט, כדאי לצפות בהדגמה הזו של רוברט פלאק, שמדגים איך אלמנטים מתנהגים באופן שונה עם ובלי מיקומים "דביקים". כדי לראות את ההבדלים, צריך להשתמש ב-Chrome Canary (גרסה 56 נכון למועד הכתיבה) או ב-Safari.

צילום מסך של נקודת המבט של Parallax

הדגמה מאת רוברט פלאק שמראה איך position: sticky משפיע על גלילת פרלקס.

מגוון באגים ופתרונות עקיפים

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

  • התמיכה הדביקה לא עקבית. התמיכה ב-Chrome עדיין מיושמת ב-Chrome, אין תמיכה מלאה ב-Edge, וב-Firefox יש באגים בציור כאשר שילוב "דביק" עם שינויי פרספקטיבה. במקרים כאלה, כדאי להוסיף קוד קטן כדי להוסיף את position: sticky (הגרסה עם הקידומת -webkit-) רק במקרה הצורך, המיועד ל-Mobile Safari בלבד.
  • האפקט לא "רק עובד" ב-Edge. Edge מנסה לטפל בגלילה ברמת מערכת ההפעלה, וזה בדרך כלל דבר טוב, אבל במקרה הזה הוא מונע ממנה לזהות את השינויים בפרספקטיבה במהלך הגלילה. כדי לתקן את הבעיה, תוכלו להוסיף רכיב של מיקום קבוע, כי נראה שהדברים האלה מעבירים את Edge ל שיטת גלילה שאינה מבוססת על מערכת ההפעלה, כדי להבטיח שהיא לוקחת בחשבון שינויים בפרספקטיבה.
  • "התוכן של הדף נהיה עצום!" דפדפנים רבים לוקחים בחשבון את קנה המידה שקובעים את גודל התוכן בדף, אבל לצערנו, Chrome ו-Safari לא לוקחים בחשבון את נקודת המבט. אם יש - למשל, סולם שהופיע פי 3 על רכיב מסוים, יכול להיות שתראו סרגלי גלילה וכדומה, גם אם הרכיב נמצא בגודל 1x אחרי החלת ה-perspective. אפשר לעקוף את הבעיה על ידי שינוי קנה המידה של הרכיבים מהפינה השמאלית התחתונה (עם transform-origin: bottom right), מכיוון שהפעולה הזו גורמת לרכיבים גדולים מדי להפוך ל'אזור השלילי' (בדרך כלל בפינה השמאלית העליונה) של האזור ניתן לגלילה. אזורים שניתנים לגלילה אף פעם לא מאפשרים לראות או לגלול לתוכן באזור השלילי.

סיכום

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

שחקו וספר לנו איך אתם מתקדמים.