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

מריקו קוסאקה

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

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

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

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

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

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

תהליך הרינדור
איור 1: תהליך כלי הרינדור עם thread ראשי, שרשורים של worker, שרשור קומפוזיציה ושרשור של רסטר בפנים

ניתוח

בניית 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, אבל כדי להאיץ את התהליך, 'סורק הטעינה מראש' פועל בו-זמנית. אם במסמך ה-HTML יש פריטים כמו <img> או <link>, סורק הטעינה מראש מציץ באסימונים שנוצרו על ידי מנתח ה-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 וגודל של תיבות תוחמות. עץ הפריסה יכול להיות דומה למבנה של עץ ה-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, פריסה ועץ צבע, לפי סדר היצירה

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

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

jage jank מחסרות פריימים
איור 11: פריימים של אנימציה בציר זמן

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

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

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

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

איחוד

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

איור 14: אנימציה של תהליך נאיביות רסטר

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

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

מה זה איחוד

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

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

בחלונית Layers תוכלו לראות איך האתר מחולק לשכבות ב-DevTools.

חלוקה לשכבות

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

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

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

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

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

רסטר
איור 17: שרשורי רסטר שיוצרים את מפת הסיביות של משבצות ושולחים אל GPU

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

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

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

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

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

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

סיכום

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

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

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

הבא: הקלט מגיע לקומפוסטר