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

מריקו קוסאקה

הקלט מגיע למרכיב

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

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

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

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

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

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

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

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

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

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

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

חשוב לשים לב כשכותבים גורמים מטפלים באירועים

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

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

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

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

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

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: ה-thread הראשי שמביט ברשומות הצבע, ושואל מה משורטט בנקודת x.y

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

צמצום ככל האפשר של שליחת אירועים ל-thread הראשי

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

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

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

כדי לצמצם את מספר הקריאות הגבוהות ל-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.
    }
});

השלבים הבאים

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

שימוש ב-Lighthouse

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

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

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

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

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

סיכום

תודה

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

תודה רבה לכל מי שבחן את הטיוטות המוקדמות של הסדרה, כולל (בין היתר): Alex Russell, Paul Ireland, Meggin Kearney, Eric Bidelman, Mathias Bynens, Addy Osmani, Addy Osmani,, Kinu1ko} Kinuko} Kinuko},

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