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

Mariko Kosaka

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

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

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

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

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

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

המרכיב מקבל אירועי קלט

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

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

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

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

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

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

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

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

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

אזור מלא בדף שלא ניתן לגלילה מהירה
איור 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: אירועים שגורמים לשיטפון בציר הזמן של המסגרת, וכתוצאה מכך לתנודות בדף

כדי לצמצם קריאות מוגזמות ל-thread הראשי, 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 שחוסם את המנתח. כך הקוד לא חוסם את המנתח, והדפדפן לא צריך לדאוג להשהיית המנתח.

סיכום

תודה

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

תודה רבה לכל מי שבדק טיוטות מוקדמות של הסדרה הזו, כולל (בין היתר): Alex Russell,‏ Paul Irish,‏ Meggin Kearney,‏ Eric Bidelman,‏ Mathias Bynens,‏ Addy Osmani,‏ Kinuko Yasuda,‏ Nasko Oskov ו-Charlie Reis.

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