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

Mariko Kosaka

הפעילות הפנימית של תהליך כלי הרינדור

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

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

תהליכי כלי הרינדור מטפלים בתוכן מהאינטרנט

תהליך הרינדור הוא אחראי לכל מה שקורה בתוך כרטיסייה. בתהליך הרינדור, ה-thread הראשי מטפל ברוב הקוד שאתם שולחים למשתמש. לפעמים חלקים מ-JavaScript מעובדים על ידי שרשורי עובדים אם אתם משתמשים ב-Web Worker או ב-service worker. שרשורים של קומפוזבילי ו-Raster פועלים גם בתוך תהליכים של רינדור כדי לעבד דף בצורה יעילה וחלקה.

המשימה העיקרית של תהליך הרינדור היא להפוך את ה-HTML, ה-CSS ו-JavaScript לדף אינטרנט שהמשתמש יכול לבצע איתו אינטראקציה.

תהליך הרינדור
איור 1: תהליך כלי הרינדור עם שרשור ראשי, שרשורי עובדים, שרשור של מחבר ושרשור בתוך רשת

ניתוח

בניית DOM

כשתהליך הרינדור מקבל הודעת התחייבות לגבי ניווט ומתחיל לקבל נתוני HTML, ה-thread הראשי מתחיל לנתח את מחרוזת הטקסט (HTML) ולהפוך אותה ל-Document Object Model (DOM).

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

ניתוח של מסמך HTML ל-DOM מוגדר על ידי תקן HTML. יכול להיות שהבחנתם שהזנת HTML בדפדפן אף פעם לא גורמת לשגיאה. לדוגמה, תג הסגירה </p> חסר הוא קוד HTML חוקי. תגי עיצוב שגויים כמו Hi! <b>I'm <i>Chrome</b>!</i> (התג b נסגר לפני i Tag) נחשבים כאילו כתבתם Hi! <b>I'm <i>Chrome</i></b><i>!</i>. הסיבה לכך היא שמפרט ה-HTML נועד לטפל בשגיאות האלה באלגנטיות. אם מעניין אתכם איך לעשות את זה, אתם יכולים לקרוא את הקטע מבוא לטיפול בשגיאות ומקרים מוזרים במנתח במפרט ה-HTML.

משאב המשנה בטעינה

בדרך כלל, אתרים משתמשים במשאבים חיצוניים כמו תמונות, CSS ו-JavaScript. צריך לטעון את הקבצים האלה מהרשת או מהמטמון. ה-thread הראשי יכול לבקש אותם אחד אחרי השני כי הם מוצאים אותם במהלך הניתוח לבניית DOM, אבל כדי לזרז את התהליך, 'preload Scanner' פועל בו-זמנית. אם יש פריטים כמו <img> או <link> במסמך ה-HTML, סורק הטעינה מראש מציץ באסימונים שנוצרו על ידי מנתח ה-HTML ושולח בקשות לשרשור הרשת בתהליך הדפדפן.

DOM
איור 2: ה-thread הראשי לניתוח HTML ובניית עץ DOM

JavaScript יכול לחסום את הניתוח

כשמנתח ה-HTML מוצא תג <script>, הוא משהה את הניתוח של מסמך ה-HTML והוא צריך לטעון, לנתח ולהפעיל את קוד ה-JavaScript. למה? כי JavaScript יכול לשנות את צורת המסמך באמצעות דברים כמו document.write(), שמשנה את מבנה ה-DOM כולו (סקירה כללית של מודל הניתוח במפרט ה-HTML כוללת תרשים יפה). לכן המנתח של ה-HTML צריך להמתין להרצת JavaScript לפני שהוא יוכל להמשיך לנתח את מסמך ה-HTML. כדי לדעת מה קורה בהפעלת JavaScript, צוות V8 מנהל דיונים ופוסטים בבלוגים בנושא הזה.

רמז לדפדפן איך לטעון משאבים

יש דרכים רבות שמפתחי אינטרנט יכולים לשלוח רמזים לדפדפן כדי לטעון משאבים בצורה טובה. אם JavaScript לא משתמש ב-document.write(), אפשר להוסיף את המאפיין async או defer לתג <script>. לאחר מכן הדפדפן טוען ומפעיל את קוד ה-JavaScript באופן אסינכרוני ולא חוסם את הניתוח. אפשר גם להשתמש במודול JavaScript, אם זה מתאים. בעזרת <link rel="preload"> אפשר ליידע את הדפדפן שהמשאב נדרש לצורך הניווט הנוכחי ושברצונך להוריד אותו בהקדם האפשרי. אפשר לקרוא מידע נוסף בנושא הזה במאמר תעדוף משאבים – לעזור לדפדפן.

חישוב הסגנון

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

הסגנון המחושב
איור 3: ה-CSS הראשי שמנתח את ה-thread הראשי כדי להוסיף סגנון מחושב

גם אם לא מספקים CSS, לכל צומת DOM יש סגנון מחושב. כאשר תג <h1> מוצג גדול יותר מהתג <h2>, והשוליים מוגדרים לכל רכיב. הסיבה לכך היא שלדפדפן יש גיליון סגנונות שמוגדר כברירת מחדל. אם אתם רוצים לדעת איך פועל שירות ה-CSS שמוגדר כברירת מחדל ב-Chrome, אפשר לראות את קוד המקור כאן.

פריסה

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

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

הפריסה היא תהליך של איתור הגיאומטריה של אלמנטים. ה-thread הראשי עובר דרך ה-DOM והסגנונות המחושבים ויוצר את עץ הפריסה שכולל מידע כמו קואורדינטות x y וגדלים של תיבות תוחמות. עץ הפריסה יכול להיות דומה למבנה של עץ DOM, אבל הוא מכיל רק מידע שקשור למה שגלוי בדף. אם מחילים את display: none, הרכיב הזה אינו חלק מעץ הפריסה (עם זאת, רכיב עם visibility: hidden נמצא בעץ הפריסה). באופן דומה, אם מיושם רכיב פסאודו עם תוכן כמו p::before{content:"Hi!"}, הוא ייכלל בעץ הפריסה למרות שהוא לא נמצא ב-DOM.

פריסה
איור 5: ה-thread הראשי שעובר מעל עץ DOM עם סגנונות מחושבים ויצירת עץ פריסה
איור 6: פריסת תיבה לפסקה שזזה עקב שינוי מעבר שורה

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

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

צבע

משחק ציור
איור 7: אדם מול קנבס שמחזיק מברשת ציור, ותוהה אם צריך לשרטט קודם עיגול או ריבוע

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

לדוגמה, יכול להיות שהפונקציה z-index מוגדרת לרכיבים מסוימים. במקרה כזה, ציור לפי סדר הרכיבים שנכתבו ב-HTML יגרום לעיבוד שגוי.

כשל ב-z-index
איור 8: רכיבי דף מופיעים לפי סדר תגי עיצוב של HTML, וכתוצאה מכך עיבוד התמונה שגוי כי z-index לא נלקח בחשבון

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

רשומות צבע
איור 9: ה-thread הראשי שעובר דרך עץ הפריסה ויוצר רשומות צבע

עדכון צינור עיבוד הנתונים יקר

איור 10: עצים מסוג DOM+Style, 'פריסה' ו'צבע' לפי סדר היצירה

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

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

jage jank על ידי חסר פריימים
איור 11: פריימים עם אנימציה על ציר הזמן

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

jage jank על ידי JavaScript
איור 12: פריימים של אנימציה על ציר הזמן, אבל פריים אחד חסום על ידי JavaScript

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

בקשת מסגרת לאנימציה
איור 13: מקטעי JavaScript קטנים יותר שפועלים על ציר זמן עם מסגרת אנימציה

הרכבה

איך משרטטים דף?

איור 14: אנימציה של תהליך עיבוד נתונים נאיבי

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

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

מה זה הרכבה

איור 15: אנימציה של תהליך ההרכבה

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

תוכלו לראות איך האתר מחולק לשכבות בכלי הפיתוח באמצעות החלונית Layers.

חלוקה לשכבות

כדי למצוא אילו רכיבים צריכים להיות באילו שכבות, ה-thread הראשי עובר דרך עץ הפריסה כדי ליצור את עץ השכבות (החלק הזה נקרא 'Update Layer Tree' (עדכון עץ השכבות בחלונית הביצועים של כלי הפיתוח). אם לא ניתן לקבל גרסה כזו בחלקים מסוימים בדף שאמורים להיות שכבה נפרדת (כמו תפריט צד של הדף) אפשר לרמוז על כך בדפדפן באמצעות המאפיין will-change ב-CSS.

עץ שכבות
איור 16: ה-thread הראשי שעובר דרך עץ הפריסה שיוצר עץ שכבות

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

רסטר והרכבה של ה-thread הראשי

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

רסטר
איור 17: שרשורים של רסטר שיוצרים את מפת סיביות של כרטיסי מידע ושולחים אותם ל-GPU

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

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

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

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

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

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

סיכום

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

בפוסט הבא והאחרון בסדרה הזו, נבחן פרטים נוספים בשרשור של המחברים ונבין מה קורה כשקלט של משתמשים, כמו mouse move ו-click,

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

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