מבט מבפנים על דפדפן אינטרנט מודרני (חלק 4)

Mariko Kosaka

קלט מגיע למעבד הווידאו

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

אירועי קלט מנקודת המבט של הדפדפן

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

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

אירוע קלט
איור 1: אירוע קלט שמנותב דרך תהליך הדפדפן לתהליך ה-renderer

ה-Compositor מקבל אירועי קלט

איור 2: חלון התצוגה מרחף מעל שכבות הדף

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

הסבר על אזור שלא ניתן לגלול בו במהירות

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

אזור מוגבל שלא ניתן לגלול בו במהירות
איור 3: תרשים של הקלט המתואר לאזור שלא ניתן לגלול בו במהירות

דברים שכדאי לדעת כשכותבים פונקציות טיפול באירועים

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

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

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

אזור מלא בדף שלא ניתן לגלילה מהירה
איור 4: דיאגרמה של הקלט המתואר לאזור שלא ניתן לגלול בו במהירות, שכולל דף שלם

כדי למנוע זאת, אפשר להעביר את האפשרויות של passive: true למאזין האירועים. כך מאותתים לדפדפן שעדיין רוצים להאזין לאירוע בשרשור הראשי, אבל ה-compositor יכול להמשיך ולעבד גם פריים חדש.

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

בודקים אם אפשר לבטל את האירוע

גלילה בדף
איור 5: דף אינטרנט שבו חלק מהדף קבוע לגלילה אופקית

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

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

document.body.addEventListener('pointermove', event => {
    if (event.cancelable) {
        event.preventDefault(); // block the native scroll
        /*
        *  do what you want the application to do here
        */
    }
}, {passive: true});

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

#area {
  touch-action: pan-x;
}

איך מוצאים את יעד האירוע

בדיקה של מיקום המצביע
איור 6: השרשור הראשי בודק את רשומות הציור ומבקש לדעת מה מצויר בנקודה x.y

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

צמצום השליחה של אירועים לשרשור הראשי

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

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

אירועים ללא סינון
איור 7: אירועים שגורמים לשיטפון בציר הזמן של המסגרת וגורמים לתנודות בדף

כדי למזער את מספר הקריאות העודפות לשרשור הראשי, Chrome משללב אירועים רציפים (כמו wheel, ‏ mousewheel, ‏ mousemove, ‏ pointermove, ‏ touchmove) ומעכב את ההעברה עד ממש לפני האירוע הבא מסוג requestAnimationFrame.

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

אירועים נפרדים כמו keydown,‏ keyup,‏ mouseup,‏ mousedown,‏ touchstart ו-touchend נשלחים באופן מיידי.

שימוש ב-getCoalescedEvents כדי לקבל אירועים בתוך הפריים

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

getCoalescedEvents
איור 9: נתיב חלק של מחווה מגע בצד ימין, נתיב מוגבל משולב בצד ימין
window.addEventListener('pointermove', event => {
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // draw a line using x and y coordinates.
    }
});

השלבים הבאים

בסדרה הזו התמקדנו בתפעול הפנימי של דפדפן אינטרנט. אם אף פעם לא תהיתם למה DevTools ממליץ להוסיף את הערך {passive: true} למטפל באירוע, או למה כדאי לכתוב את הערך async בתג הסקריפט, אני מקווה שהסדרה הזו תאיר קצת את הנושא ותסביר למה הדפדפן זקוק למידע הזה כדי לספק חוויית שימוש מהירה וחלקה יותר באינטרנט.

שימוש ב-Lighthouse

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

איך מודדים את הביצועים

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

הוספת מדיניות התכונות לאתר

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

סיכום

תודה

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

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

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