האופן שבו פועל תהליך עיבוד
זהו החלק השלישי מתוך סדרת הבלוגים בת 4 החלקים שמסבירה איך הדפדפנים פועלים. בעבר דיברנו על ארכיטקטורה של תהליכים מרובים ועל תהליך הניווט. בפוסט הזה נבחן מה קורה בתהליך העיבוד.
תהליך היצירה של ה-Renderer משפיע על היבטים רבים של ביצועי האתר. מכיוון שקורה הרבה בתהליך היצירה, הפוסט הזה הוא רק סקירה כללית. אם אתם רוצים להעמיק את הנושא, תוכלו למצוא מקורות מידע נוספים בקטע 'ביצועים' במאמר 'עקרונות בסיסיים של האינטרנט'.
תהליכי הרינדור מטפלים בתוכן האינטרנט
תהליך היצירה אחראי לכל מה שקורה בכרטיסייה. בתהליך של המרת קוד, החוט הראשי מטפל ברוב הקוד שאתם שולחים למשתמש. אם אתם משתמשים ב-web worker או ב-service worker, לפעמים חלקים מ-JavaScript מטופלים על ידי חוטי עבודה. גם שרשראות של רכיבי עיבוד ושרשראות רסטר פועלות בתוך תהליכי עיבוד כדי להציג דף ביעילות ובקלות.
התפקיד העיקרי של תהליך העיבוד הוא להפוך את קובצי ה-HTML, ה-CSS ו-JavaScript לדף אינטרנט שהמשתמש יכול לקיים איתו אינטראקציה.

ניתוח
בניית 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, ושולח בקשות לשרשור הרשת בתהליך הדפדפן.

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
בכלי הפיתוח.

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

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

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

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

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

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

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

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

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

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

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

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