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

Mariko Kosaka

האופן שבו פועל תהליך עיבוד

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

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

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

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

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

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

ניתוח

בניית DOM

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

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

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

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

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

DOM
איור 2: השרשור הראשי מנתח 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. החוט הראשי מנתח את ה-CSS ומחליט מהו הסגנון המחושב של כל צומת DOM. זהו מידע על סוג הסגנון שחלה על כל רכיב על סמך סלקטורים ב-CSS. המידע הזה מוצג בקטע computed בכלי הפיתוח.

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

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

פריסה

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

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

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

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

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

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

צבע

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

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

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

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

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

רשומות של צביעה
איור 9: השרשור הראשי עובר בעץ הפריסה ויוצר רשומות של ציור

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

איור 10: עצי DOM+Style,‏ Layout ו-Paint בסדר שבו הם נוצרים

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

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

תנודות חדות בתמונה עקב מסגרות חסרות
איור 11: פריימים של אנימציה בציר זמן

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

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

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

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

עיבוד (compositing)

איך תציירו דף?

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

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

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

מהו קומפוזיציה

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

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

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

חלוקה לשכבות

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

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

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

יצירת רסטר ושילוב מחוץ ל-thread הראשי

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

רסטר
איור 17: שרשורי Raster יוצרים את קובץ ה-bitmap של המשבצות ושולחים אותו ל-GPU

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

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

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

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

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

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

סיכום

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

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

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

השלב הבא: הקלט מגיע למעבד התמונה