2013-06-01

רינדור בצד הדפדפן

פוסט זה משלים בעצם 2 נושאים שונים שכתבתי עליהם לאחרונה:
  • הפוסט משלים את נושא מנוע הרינדור מהפוסט הקודם (א.ה.ש.ה: "הדפדפן").
  • הפוסט משלים את הפוסטים על "ביצועים של אפליקציות אינטרנט" ונותן טיפים חשובים לשיפור זמני-רינדור בדפדפן.
לאחר חשיבה, הוספתי את 2 הפוסטים על "ביצועים של אפליקציות אינטרנט" לסדרה (במקום לפתוח סדרה חדשה על ביצועים בווב) - זאת מכיוון שלא ניתן לעסוק בביצועים ללא הבנה של ה internals, ומכיוון שהבנה של ה internals מובילה מפתחים לשיפורי ביצועים.

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


פוסט זה שייך לסדרה אבני הבניין של האינטרנט.





שינוי פרדיגמה

במשך שנים נהגנו למדוד ביצועי אפליקציות ווב ע"פ:
  • מספר ה round-trips בין הדפדפן לשרת - שאפנו בקנאות למינימום.
  • כמות התעבורה ברשת ב kb - עמלנו על מנת לצמצם.
  • CPU של תהליך הדפדפן - שייחסנו אותו ברובו למשהו שמעבר לשליטתנו.
כל זה טוב ויפה סביר כאשר מדובר בדפי HTML פשוטים יחסית שנבנים בצד השרת, נוסח JSP או ASP.NET.

באפליקציות ווב מודרניות המצב התהפך:

רוב זמן הרינדור הוא באופן ברור בצד הלקוח.
ע"פ Alexa, כ 80% מזמן רינדור הדף ב 1000 האתרים המובילים מושקע בצד הלקוח.

חלק נכבד מהשיפורים האפשריים - הוא בצד הלקוח.
לא מזמן נתקלתי במקרה בו הצליחו להוריד את זמן הרינדור של דף מ 11 ל 2.5 שניות ע"י שינוי בקוד הג'אווהסקריפט בלבד. זהו מקרה מעט קיצוני, אך אמיתי.
באפליקציות ווב, ניתן לשפר רבות את "הזרימה החלקה" של האפליקציה ע"י שינויים קטנים ב CSS ובקוד הג'אווהסקריפט שמשתמש ב DOM API - רק צריך להכיר כמה עקרונות שאסביר בפוסט זה. שינויים אלו משמעותיים במיוחד לשימוש ב touch בהם עיקובים ברינדור ("תקיעה") היא מציקה במיוחד מכיוון שהציפיה היא שכל האפליקציה "תעבוד חלק"...

מקור


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

דף שעולה מהר, מול דף שנותן הרגשה שהוא עולה מהר. מקור.

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



מדוע רינדור הדף "עוצר" בזמן טעינת הדף?

כפי שתיארנו בפוסט קודם, קובץ ה HTML עובר פירסור והוא זה שגורם לטעינת קבצי הג'אווהסקריפט / CSS:
שלב 2: כאשר ה HTML Parser נתקל בתגיות המתארות קבצים אחרים - הוא טוען אותם.

אלמנט בעייתי בשלב טעינת קבצי הג'אווהסקריפט הוא היכולת של הסקריפט לשנות את קובץ ה HTML - במיוחד בעזרת פקודת document.write. בפקודה זו, שנראית תמימה לכאורה, יש מלכוד: היא מוסיפה markup לסוף ה DOM Tree ולכן יש משמעות שונה אם נריץ אותה עכשיו, או בעוד כמה מילי-שניות, לאחר שעוד markup פורסר והוסף לסוף ה DOM Tree.

בכדי להבטיח את נכונות טעינת הדף, ברגע שהדפדפן מזהה בקשה לטעינת סקריפט כלשהו (תגית <script>) ועד רגע שהסקריפט סיים לרוץ - הוא "מקפיא" את מנוע פירסור ה HTML. הקפאה זו באה ברגע קריטי ביותר: טעינת הדף הראשונית!
התוצאה האפשרית: מסך לבן למשתמש הקצה עד אשר כל הסקריפטים ירוצו (חוויה מוכרת מאתרים רבים) = perceived performance גרוע. מצב זה מחמיר ככל שהאפליקציה גדולה וטוענת כמות גדולה של ג'אווהסקריפט.
אפרופו: Internet Explorer, עד IE8, הגדיל לעשות והפסיק גם את טעינת הרשת של משאבים אחרים. עד היום לא ברור מדוע.

ישנן מספר דרכים להתמודד עם הבעיה הזו:
  1. להעביר את הבקשות לטעינת סקריפטים מה HEAD לתחתית ה HTML, ממש בסוף ה BODY [א].
  2. לציין על הסקריפטים ש "הם ילדים טובים" בעזרת תווית async (מייד)
  3. להשתמש בספריה כגון require.js שעושה את שני הדברים עבורכם [זהירות, פוסט].

תווית async ניתן לשים על סקריפטים ש:
  • לא מבצעים כתיבה ל DOM בעזרת document.write בזמן ההרצה.
  • סקריפטים אחרים לא תלויים בהם - בשלב ההרצה של הסקריפטים האחרים. כלומר הסקריפט האחר יכול לכלול פונקציות שקוראות לסקריפט שלנו, אל לא לפני שהיה ארוע document.ready.
אם הקוד שלכם מובנה ומסודר - קרוב לוודאי שזה כבר המצב.

המשמעות של תווית ה async היא שהדפדפן יטען את הקבצי הג'אווהסקריפט באופן מקבילי לפירסור HTML (ואחד לשני). כלומר - אין הבטחה באיזה סדר הם ירוצו. יש לנקוט בקוד הג'אווהסקריפט מעט מאמצי הגנה כגון הגדרת namespace בכל קובץ או וידוא שקוד ה bootstrap קשור לאירוע של הדפדפן (כגון document.ready) - לרוב זה לא מאמץ גדול.

תווית נוספת היא תווית defer שמאפשרת לסמן סקריפטים שיכולים לרוץ מאוחר יותר (כגון כפתור "feedback"). סקריפטים אלו יטענו במקביל כמו קבצים שסומנו כ async אך ירוצו רק כאשר כל משימות הפירסור הנוכחיות - הסתיימו.


-----
הערת צד: בעיית "עצירת פירסור ה HTML" היא כ"כ חמורה שהדפדפנים מנסים בכל זאת להתמודד איתה, גם בלי עזרת המפתחים:

Firefox: ברגע שהוא מתחיל להיתקל בסקריפטים, הוא שומר עותק של ה DOM Tree בצד (להלן DT'). משם הוא ממשיך לפענח ולבנות את ה DOM Tree (להלן DT) תוך כדי שהוא עוקב אחר התנהגות הסקריפטים. 
אם לא הייתה פעולת document.write הוא משתמש ב DT - שהוא כבר הספיק לעבוד עליו בקביל בזמן שהסקריפטים נטענו.
אם הייתה פעולת document.write (המצב הנדיר) אזי הוא חוזר ל DT' וממשיך לבנות את ה DOM Tree מנקודה זו.

Chrome: ברגע שהוא נתקל ב Script ראשון, יפעיל את ה Proload Scanner שימשיך לפענח ולסרוק את ה HTML בכדי לחפש אחר קבצי סקריפט נוספים ולהתחיל לטעון את הקבצים מהרשת במקביל. "רישום הפונקציות" וההרצה של הסקריפטים עדיין תעשה ע"פ הסדר שבו הסקריפטים הופיעו במסמך ה HTML - בכדי להבטיח את תקינות ההרצה.
-----


HTML דינאמי - ההשפעה של פעולות DOM מתוך קוד ג'אווהסקריפט

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

המשמעות של כתיבה ל DOM

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

דמיינו אפליקציה עם מאות nodes ב Style Tree. כל הכנסה של אלמנט ל DOM תדרוש מעבר על כל החוקים בתת העץ ב Render Tree. אם מבוצעות הכנסות חוזרות לאותו תת-עץ ב DOM Tree - יהיו בדיקות ונשנות של DOM Tree Nodes כנגד אותם חוקים - עבודה מיותרת בעליל.

מפתחים שמבינים שכתיבה ל DOM היא יקרה, עוברים בד"כ את השלב בו הם מנסים לבנות "תת עץ DOM" בצד ולהכניס אותו ב"מכה" אחת. למשל - כ String. פעולה זו לרוב לא תציג שיפור בתוצאות - מה שעלול להשאיר את המפתחים מבולבלים.



הסיבה שאנו לא רואים שיפור בין 2 הגישות היא מכיוון שכותבי הדפדפנים חשבו עלינו - ועשו את העבודה עבורנו:
כתיבות ל DOM Tree עוברות דרך Write Buffer שיבצע Flush (שפיכת כל השינויים שהצטברו ב buffer) בנקודת הזמן המאוחרת ביותר האפשרית.

מה שמעניין הוא מה היא" נקודת הזמן המאוחרת ביותר האפשרית"? ובכן - זו הנקודה בה קורה אחד מ 2 דברים:
  1. מישהו מבצע פעולת read מתוך ה DOM Tree או ה Render Tree - והדפדפן רוצה שהוא יקבל תשובה עדכנית לאור הכתיבות שנעשו קודם לכן.
  2. תזמון קבוע שנקבע בכדי להציג שינויים למשתמש. רוב הדפדפנים היום קבעו לעצמם קצת רענון של 60fps, כך שכל 16.67 מילי-שניות הדפדפן מעדכן את ה DOM בכדי שיוכל להציג את השינויים שנעשו על ה Canvas, כך שיגיע למשתמש הקצה.
    אופטימיזציות שונות עשויות לזהות שפעולה אינה בעלת משמעות על המסך (למשל שינוי של אלמנט שהוגדר כ display:none) וכך לדלג על פעולת flush שנובעת מתזמון.

שימו לב שקריאות שונות של ה DOM API ניגשות למבני נתונים שונים. לדוגמה:
  • קריאת getAttribute או innerHTML - תגרום לקריאה מה DOM Tree. (מסומנת כ "(Read (A" בתרשים למעלה)
  • קריאת scrollHeight או scrollWidth (שהן פשוט הדרך לקבל את הגובה / רוחב של האלמנט) - תגרום לקריאה מה Render Tree. (מסומנת כ "(Read (B" בתרשים למעלה)
הערת צד: אל תגלו לאף אחד, אבל ה Render Tree הוא בעצם אינו עץ אחד אלא מספר מבני-נתונים בעלי חפיפה-חלקית שמרכיבים את התמונה השלמה. עובדה זו אינה תורמת לדיון - ולכן אשאר עם ההפשטה של ה "Render Tree".

המסקנה מהדרך בה עובד ה DOM Tree Write Buffer היא חשובה:
  • אם נבצע 100 כתיבות ל DOM ולאחר מכן 100 קריאות - תהיה פעולת Flush אחת.
  • אם נבצע לולאה של 100 x (כתיבה + קריאה מה DOM) - יהיו 100 פעולות Flush.
ההבדל (כפי שאפשר לראות בדוגמה בפוסט, לחצו על הכפתורים האדומים) היא בין פעולה ש"תוקעת" את ה UI לחצי-שנייה עד שנייה, לבין התנהגות חלקה למדי. הבדל בין ממשק "זורם" לממשק "תקוע".



דרכי קיצור ברינדור הדף: דילוגים על שלבים לא-נחוצים.

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


פעולות של דילוג על בניית ה Render Tree וה Re-layout



הדוגמה הקלה לתיאור היא פעולות ציור. כאשר אנו משנים צבע או רקע (צבע או תמונה) של אלמנט, אין צורך לחשב מחדש את הגובה / רוחב / מיקום של כל האלמנטים על הדף (==> Re-layout). אין גם צורך לבנות מחדש תתי-עצים ב Render Tree, כי אין סיכוי שהם יישתנו. אפשר לשנות רק את הצבע/רקע על האלמנט הספציפי ו"לקפוץ" ישר לרינדור ה Canvas.

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


באופציות של כלי הפיתוח (חתכתי את התמונה רק לחלק הרלוונטי) יש לסמן את "Show Paint Rectangles" ואז כל אזור ב Canvas שצויר מחדש יופיע לשנייה מסומן במלבן אדום.
אם אתם רואים ששינוי קטן מבחינתכם גרר ציור של אזור גדול ב Canvas - כדאי לחקור את הסיבה.


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

הוספת class או attribute על אלמנט, כאשר class זה לא מחיל על האלמנט שום חוק חדש. עדיין יחושבו כל חוקי ה CSS מול האלמנט ותת העץ שלו, אך לא יבנו מחדש האלמנטים ב Render Tree, לא יהיה Layout ולא יהיה רינדור מחדש ל Canvas.



פעולות היכולות לדלג על ה Re-Layout

למשל:
  • שינוי visibility (למשל none ל visible) - מכיוון שאלמנטים אלו עדיין נשמרים ב Render Tree.
  • שינוי מיקום לאלמנט "מרחף" שאינו חלק מה Layout (או לפחות ה Layout שיושפע ממנו יהיה קטן בהרבה).
  • פעולות transform




אולי כדאי לסכם פעולות אותן כדאי לנסות ולצמצם / לרכז כאשר עובדים על שיפור ביצועי ה rendering:

פעולות שיגרמו לשינויים ב Layout:
  • שינוי גובה, רוחב
  • הוספת אלמנטים / הסרת אלמנטים נראים.
  • שינויים בטקסט (כולל, למשל, line-height או word-spacing)
  • שינויים ב scroll
  • שינויים ב border-width (אך לא בצבע או border-radius, אלו פעולות שישפיעו רק על ה render / paint)
  • שינוי פונטים
  • שינויים ב margin או padding
  • שינוי Display מערך inline ל block (או להיפך) 

פעולות שיגרמו לבניית תתי-עצים ב Render Tree
  • Display (מעבר מ/אל "none")
  • Transform (מעבר מ/אל "none) - מכיוון שצריך לעדכן את הבנים
  • float (מעבר מ/אל "none")



סיכום


הנה כמה דברים מעשיים שאתם יכולים לעשות בכדי לשפר את ביצועי אפליקציית הווב שלכם:
  1. הדפדפנים כיום כוללים כלי פיתוח מתקדמים, בעיקר כרום ופיירפוקס. מומלץ ללמוד לעבוד עם Chrome Developer Tools ו/או Firefox Developer Tools. כאשר תשפרו ביצועים - רוב הזמן שלכם יילך לשם.
  2. כלים כמו YSlow או Page Speed יתנו לכם ניתוח מהיר, בעיקר על ביצועי הרשת.
  3. נסו להקדים את טעינת ה CSS להתחלה, ואת הרצת הסקריפטים לשלב מאוחר יותר.
  4. זכרו בעת כתיבת CSS שאת החוקים הדפדפן מנתח מימין לשמאל, ונסו לשים בצד ימין תנאי שקל לשלול.
  5. נסו לצמצם את מספר החוקים ב CSS ואת עומק ה DOM - במידת האפשר.
  6. היו מודעים ל DOM Tree Write buffer וכתבו קוד כך שלא יבצע flush ללא סיבה.
  7. למדו אילו פעולות גורמות לעבודה ב Rendering Engine ונסו להשתמש בפעולות שלהן יש דרכי קיצור.
  8. קראו את הפוסט על מנוע הג'אווהסקריפט - הוא כולל כמה טיפים נוספים.


שיהיה בהצלחה!


-----

[א] כלל זה עובד מצויין, יש לשים לב שאת קבצי ה CSS אנחנו רוצים לטעון מוקדם ככל הניתן (ז.א. ב HEAD) מכיוון של הוספה של Style Rule היא יקרה ביותר, במיוחד ככל שה DOM Tree הולך וגדל.


מקורות נוספים:

Faster HTML and CSS
הרצאה טובה מאוד של דויד ברון (עובד עבור מוזילה) על תהליך והרינדור והאפטימיזציות השונות.
http://www.youtube.com/watch?v=a2_6bGNZ7bA


צמצום פעולות ה Re-layout וה Paint
כולל דוגמאות קוד.
http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/


קורס מזורז לביצועי ווב
מצגת טובה!
http://www.igvita.com/slides/2012/webperf-crash-course.pdf




2013-05-18

אבני הבניין של האינטרנט: הדפדפן

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


פוסט זה שייך לסדרה: אבני הבנין של האינטרנט.






רכיבי הדפדפן

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



Networking
מודול זה אחראי לניהול ה TCP connections (כמה לפתוח, כמה זמן להחזיק,...), ציות לכללי פרוטוקול ה HTTP הקשורים לתקשורת, וסיפוק שירותים מתקדמים כגון תקן webSockets של HTML5. לדפדפנים שונים יש מימושים שונים למודול ה Networking, המשפיעים על אופי התקשורת. ל Chromium (כלומר דפדפן כרום) יש מודול שנקרא Chromium Network Stack ולספארי יש את CFNetwork.
דפדפנים מודרניים מיישמים אופטימיזציות רבות ברמת הרשת:

  • יצירת connection מוקדם (preconnection) גם מבלי שהמשתמש לחץ על לינק. סיבות אפשריות: משתמש עבר עם העכבר מעל לינק (סיכוי גבוה שהוא ילחץ) או דפים שהמשתמש כמעט תמיד ופותח כשהוא מפעיל את הדפדפן (לדוגמה ynet).
  • השארה של TCP connection פתוח עוד כמה שניות.... אתרים רבים מבצעים קריאות Ajax זמן קצר לאחר שהאתר נטען (סוג של Lazy Loading). ה Connection כבר "חם" וחבל לסגור אותו.
  • ניחוש איזה משאב כדאי לבקש מאיזה TCP connection פתוח. לדוגמה: דחייה של בקשה לטעינת תמונות בכמה מאות מילי-שניות, כי אולי תבוא בקשה לטעינת קובץ javaScript או CSS - שיש יתרונות רבים לטעון אותם מוקדם יותר.

Storage
מודול האכסון מנהל אכסון של Caches, של תקני HTML5 כגון Local Storage או ניהול של Cookies או Plugins. למודול זה יש עבודה רבה עבור הדפדפן, אך השפעה קטנה יחסית על מפתח הווב.


Graphics
מודול זה כולל בתוכו הפעלה של ספריות הגרפיקה של מערכת ההפעלה. הן לצורך רינדור פקדים "טבעיים" של מערכת ההפעלה (למשל DropDown List) או פונטים והן לצורך הפעלה של האצת חומרה [א] שתסייע למנוע הרינדור.


מנוע הרצת הג'אווהסקריפט (javaScript Engine, לשעבר javaScript Interpreter)
המודול האחראי להרצת ג׳אווהסקריפט, נעסוק בו בפוסט נפרד.


מנוע הרינדור (Rendering Engine), ידוע גם כ Layout Engine
המנוע שהופך דפי HTML לתמונה על גבי המסך, נעסוק בו בהמשך פוסט זה.


Browser Management Layer
שכבה המנהלת שירותים שונים של הדפדפן כגון:
  • Bookmarks
  • ניהול היסטורית גלישה
  • סנכרון ה Bookmarks מול שרת מרוחק (פיירפוקס, כרום ולאחרונה גם ספארי)
  • ניהול Plug-ins
  • התמודדות עם קריסות
  • וכו'


Browser Shell
בעצם שכבת ה UI, ה "פנים" של הדפדפן כפי שאנו מכירים אותו:
  • טאבים
  • כפתורי "קדימה", "אחורה", Home וכו'
  • תיבת הכתובת / חיפוש וכיצד הן מתנהגות
  • למשל: הקלדה של כתובת ב Chrome או לחיצה על כפתור "refresh" תגרום לטעינה לדף בהתעלמות מה cache בעוד שטעינת דף בעקבות לחיצה על link תגרום לטעינת הדף תוך שימוש ב cache. התנהגות זו היא חלק מהגדרת ממשק המשתמש.
  • Viewers שונים, לדוגמה הצגת תמונות או PDF הן לרוב חלק מה Shell ולא ממנוע הרינדור.
מספר לא מבוטל של דפדפנים (maxthon, iCabMobile ולאחרונה גם אופרה) הם וריאציה של דפדפן אחר ומחדשים בעיקר ב Shell ובשכבת הניהול.


ה Shell של פיירפוקס, כפי שהוא נראה על מכשירים שונים. מקור.


מנוע הרינדור

אלמנט מרכזי מאוד בעבודת הדפדפן היא עבודתו של מנוע הרינדור (ידוע כ Rendering Engine או Layout Engine). מנוע הרינדור מקבל קובץ HTML וקובצי javaScript, CSS והופך אותם לגרפיקה על המסך.
יש הרבה מנועי רינדור, הנה הרשימה של כמה מהנפוצים שבהם:


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

פוסט זה מתבסס בעיקר על ההתנהגות הידועה של Firefox ו Chrome.



ה flow המרכזי של מנוע הרינדור הוא כדלהלן:


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


שלבי הטעינה


שלב 1: פענוח קובץ ה HTML ובניית ייצוג היררכי שלו בזיכרון (מבנה נתונים הנקרא DOM Tree)
הנה תרשים המתאר את פעולת הפענוח של קובץ ה HTML. הלוגיקה כוללת נקודות רבות של "התאוששות מתקלות" (שלא באות לביטוי בתרשים למטה) כגון תגיות שלא נסגרו / לא נסגרו בסדר הנכון או הימצאותם של אלמנטים לא תקניים ב markup:
מקור: גוגל
בעבר לא היה תקן אחיד כיצד יש לפרסר HTML, וכבר בתהליך הפירסור היו נקודות שוני בין דפדפנים שונים. HTML5 הגדיר במדויק כיצד פירסור צריך להתבצע ונראה שהדפדפנים המודרניים כיום אחידים למדי ב DOM Tree שהם בונים.

שלב 2: טעינת קבצי CSS
כאשר נתקלים ב DOM באלמנטים של Sytle (קובצי CSS) - מבקשים מרכיב הרשת לטעון אותם. כשהטעינה מסתיימת - תוכן הקובץ נשלח לפענוח.

שלב 3: פענוח קבצי ה CSS
קבצי ה CSS מפוענחים בצורה דומה ל HTML (אם כי התהליך פשוט יותר) ומהם נבנה מבנה נתונים המחזיק את כל החוקים המתוארים בקבצי ה CSS, מבנה הידוע לרוב כ Style Tree או Style Rules. ה nodes ב Style Tree הם פריטי CSS Selectors כגון "p" או "hover:" ועל כל node יש את סט החוקים שפועלים על selection זה, כגון "border-radius: 4px".


שלב 4: חיבור ה Styles ל DOM ובניית ה Rendering Tree

ה Rendering Tree הוא שלב ביניים בין ה DOM Tree וה styles לציור התוכן בפועל על המסך. העץ מורכב מאלמנטים, שכל אחד מהם מתאר מלבן שמאוחר יותר יצויר על המסך:

מקור: http://dbaron.org/talks/2012-04-22-mozcamp-latam/slide-8.xhtml

אלמנטים ב Render Tree מתייחסים לאלמנטים ב DOM Tree, אך יחס זה הוא לא 1:1. ייתכן מצב בו אלמנט יחיד ב DOM (בדוגמה: אלמנט ה HTML) מיוצג ע"י מספר אלמנטים ב Render Tree. מצד שני, אלמנטים שהם hidden (תגים כמו Script או Head שהם מוחבאים כהגדרה או תגים רגילים שהוחבאו בעזרת הגדרה, כגון תכונת "display:none") פשוט לא יתוארו ע"י ה Render Tree. ה Render Tree מתאר רק את מה שאמור להיות מצויר על המסך.

יוצא דופן היא תכונת ה visibility שאיננה גורמת לכך שלא יווצר Render Tree Node. הסיבה לכך היא שע"פ התקן, בן לאלמנט "visibility:hidden" יכול עדיין להיות בעל "visibility:visible" ולהיות מוצג. כדי לחשב כיצד להציג אותו עדיין צריך ב Render Tree את פרטי האב ה"בלתי נראה".

כאשר רוצים להחיל styles על ה DOM Tree, עוברים node אחר node בתוך ה DOM ומחפשים אלו styles רלוונטיים. את החיפוש מתחילים מתחתית העץ (המקבילה לצד הימני של ה Selector) - מכיוון שכך ניתן לפסול מהר יותר Selectors שאינם רלוונטיים.


CSS Selectors וביצועים

המלצה ידועה למפתחי ווב היא:
  1. "השתמשו כמה שיותר ב ids" - חיפוש שלהם הוא הכי מהיר
  2. "אם אי-אפשר להשתמש ב id, השתמשו ב class" - גם הם מהירים
  3. "אח"כ השתמשו בכל השאר."
המלצה זו היא טובה ככלל אצבע, אך יש עוד לדעת ולעשות על מנת לשפר את ביצועי הדף / אפליקציה שלנו.

ids, למשל, הם מהירים מבחינת ביצועים אך לא כ"כ טובים מבחינת תחזוקת קוד. ישנה דעה רווחת שברגע שהדפדפן מצא את ה id המבוקש - הוא מפסיק לחפש, אולם אם תנסו להכניס 2 ids זהים ב HTML (דבר לא-חוקי בעליל) סביר שתראו שהדפדפן מרנדר את ה style נכון - מה שמצביע שזו לא הדרך שבה עובד הדפדפן פנימית. עובדה זו נכונה אולי לפקודה document.getElementById ועל כן היא קצת יותר מהירה.

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


ההשפעה של Selectors מורכבים

בואו נתאר את מבנה ה DOM ורשימת החוקים הבאים:

מקור: http://dbaron.org/talks/2008-11-12-faster-html-and-css/slide-8.xhtml

הערה: השתמשתי בדוגמה בה ה Selectors מה Style Tree מיוצגים כרשימה, כך שיהיה קל יותר לקרוא אותה.

בואו נבחן את הקושי לדעת אם אלמנט בודד ב DOM Tree מתאים ל Selectors:
  • את 4 ה Selectors הראשונים הכי קל לאשר / לבטל מידית. בחינת ה node הנוכחי מספיקה בכדי להכריע אם החוק תקף או לא. (O(1.
  • חוקים כמו "sidebar p#" דורשים טיול, פוטנציאלי, עד לראש העץ בכדי לדעת אם הם תקפים, כלומר (O(n למרות שיש לנו שני אלמנטים שראינו קודם שכ"א מחושב ב (O(1 כשהוא לבד.
  • ביטוי כמו "ul > p" ניתן לבדוק ב (O(2 - בניגוד לביטוי דומה למדי "ul p" שדורש (O(n.
  • ביטוי מורכב יותר כמו "item div p." ידרוש בתאוריה (O(n^2 מכיוון שעבור כל div בשרשרת האבות צריך להתחיל מחדש חיפוש אחר אב עם class בשם "item", אולם דפדפנים מודרניים יכולים בקלות לבצע אופטימיזציה ל (O(n, מכיוון ששרשרת האבות היא אחת.
  • עוד מקרה מיוחד selector כגון "hover:" שדורש בדיקה כל זמן שהעכבר זז, מה שעלול להפוך selector כגון "someClass div p:hover." למטרד עבור הדפדפן.
ניתוח זה נועד להבהיר את המחיר של הכנסת כל node חדש ל DOM Tree. כאשר מדובר שאילתות על ה DOM Tree מתוך javaScript, המכיר יכול להיות גדול עד פי מספר ה nodes ב DOMTree.
אם אתם עובדים עם jQuery או Zepto בכדי לתשאל את ה DOM בזמן ריצה (כדאי שתעבדו!) - כדאי להכיר כמה טיפים ספציפיים לגבי ספריות אלו.


מה אפשר לעשות בכדי לסייע ל DOM Tree להבנות מהר יותר?
  • כל אלמנט חדש שנכנס ל DOM - נבדק ע"י כל החוקים ב Style Tree. זכרו שדפדפנים קוראים את ה selectors מימין לשמאל ונסו שהאלמנט הימני ביותר ב selections שלכם יהיה ספציפי ככל הניתן, כך שיהיה אפשר לבטל אותו מהר.
  • נסו להימנע מחלקים "ברורים מאליהם" בחוקים שלכם, לדוגמה: אם "sidebar div ul li.item img." זהה בפועל ל "item img." - נסו להשתמש בשני. אני מודה שתיאור מפורט של המבנה לפעמים מסייע לכתוב קוד קריא יותר.
  • נסו להימנע, אם אפשר, מיצירת DOM Tree עמוק במיוחד.
  • נסו להימנע מחוקים מסובכים, נאמר:"{ div:nth-of-type(3) ul:last-child li:nth-of-type(odd) *{ font-weight:bold".ע"י חישוב המצב הספציפי בעצמכם וסימון האלמנטים שעומדים בתנאי ב class - אתם יכולים לחסוך לדפדפן הרבה מאוד עבודה בעת הכנסת אלמנטים ל DOM Tree.

עוד כלל חשוב הוא לטעון את כל קבצי ה CSS מוקדם ככל האפשר, ולפני קבצי ה JavaScript בפרט. מדוע?
  • הוספה של אלמנט ב DOM (נאמר בעקבות קוד ג'אווהסקריפט) גורמת לבנייה של node חדש ב Render Tree, מה שדורש מעבר על כל ה Style Tree, עם האופטימיזציות שהוזכרו לעיל. נציין עבודה זו כ w.
  • הוספה של חוק ל Style Tree דורשת לעבור על כל האלמנטים ב DOM Tree ולבדוק כל אחד מהם את החוק החדש. אם החוק תקף, יש לבנות מחדש את ה Render Tree nodes של כל הבנים של האלמנט עליו תקף החוק החדש - בגלל שתכונות ב CSS הן נורשות. רק תוספת אחרונה זו היא עבודה של m * w (כאשר m הוא מספר ה nodes שעליהם תקף החוק והבנים שלהם) - הרבה יותר מהכנסה של אלמנט ל DOM Tree.
על כן נעדיף תמיד "לסגור" את ה Render Tree שלנו לפני שאנו "נוגעים" ב DOM.

לדפדפנים יש גם עוד מנגנון שניתן לתאר כ DOM Tree write Buffer (עליו נדבר בפוסט ההמשך) שהופך סדרה של כתיבות ל DOM לזולות יותר.



שלב 5: Layout / Reflow


אלמנטים ב Render Tree יכללו הן את תכונות ה CSS Box Model: גובה, רוחב, עובי מסגרת, ריווח וכו' והן תכונות מיקום (position, float, left, top וכו').
ה CSS Box Model. מקור: http://hydrogen.informatik.tu-cottbus.de/wiki/index.php/CSS_Box_Model

גם ערכי תכונות ה box model וגם ערכי תכונות המיקום מושפעות מאלמנטים אחרים ב Rendering Tree. בתהליך שנקרא Layout או Reflow עובר מנוע הרינדור על כל האלמנטים ב Render Tree ומחשב את הרוחב והמיקום שלהם. תכונות כגון float:right (תכונת מיקום) או height:auto (תכונות גודל) משפיעות רבות על ה Layout שייווצר בסוף.
בתהליך בניית ה Render Tree מבוצע חישוב של הרוחבים של האלמנטים.
רוב האלמנטים ב Render Tree יהיו אחד מ - 2 סוגים:
  • Inline: אלמנט שתופס את הרוחב והגובה שלו, אך מאפשר להמשיך את ה inline Flow. כלומר: אם יש כמה אלמנטי Inline אחד אחר השני, הם יסתדרו בשורה כל עוד רוחב העמוד מאפשר זאת (ואז יגלשו לשורה הבאה).
  • Block: אלמנטים הדורשים שורה משלהם.

מקור (הכולל עוד מידע)

על סוגים נוספים, פחות נפוצים, של התנהגות layout ניתן לקרוא כאן.



שלב 6: Canvas & Paint


השלב האחרון של הדפדפן הוא לקחת את ה Render Tree כסדרה של הגדרות ולצייר אותו בפועל על ה canvas של הדפדפן. שלב זה נעשה בעזרת APIs של מערכת ההפעלה ושימוש ב GPU (האצת חומרה) ,אם ניתן - עבור פעולות רינדור פעולות מורכבות כגון Animate או Transform של CSS.



סיכום

בפוסט זה סקרנו את מבנה הדפדפן וה flow החשוב ביותר שלו "הפיכת HTML למסך מרונדר".
בכדי לפשט את הדיון התעלמנו מאלמנט חשוב: האופי הדינמי של הדף הנובע מקוד javaScript.
אנסה בפוסט המשך להוסיף מורכבות נוספת זו להשלמת התמונה.

שיהיה בהצלחה!



---

[א] האצת חומרה (hardware acceleration) היא הפעלת פקודות גרפיות, בעיקר, שהמעבד הגרפי (GPU: Graphical Processing Unit), יחידת עיבוד חיצונית ל CPU שיש כיום כמעט לכל מחשב - יכולה לבצע טוב יותר. שימוש ב GPU יכול לשפר משמעותית את ביצועי הרינדור של כל מיני פילטרים מתקדמים של CSS3 כגון Transform או Animations, עבודה עם Canvas או תלת-מימד.


--

מקורות מעניינים נוספים בנושא:

Browsers architecture
חלק ראשון של פוסט קלאסי המתאר את הארכיטקטורה של הדפדפנים. חבל מאוד שהחלק השני לא נכתב מעולם (הממ... גם לי זה קורה)
http://www.vineetgupta.com/2010/11/how-browsers-work-part-1-architectur

How browsers works
כתבה קלאסית ומקיפה של טלי גרסיאל על עבודתם של דפדפנים. פול אייריש (גורו ווב שעובד בגוגל) אהב את הכתבה ופרסם אותה מחדש באתר הפופולרי HTML5 Rocks!
http://www.html5rocks.com/en/tutorials/internals/howbrowserswork/
מעניין לציין שטלי ואני עבדנו במשך תקופה ״בערך״ באותה החברה (אינקפסולה ואימפרבה), אך לא הזדמן לי להכיר אותה.

(How Webkit works (ppt
קצת פרטים ספציפיים על Webkit
https://docs.google.com/presentation/pub?id=1ZRIQbUKw9Tf077odCh66OrrwRIVNLvI_nhLm2Gi__F0#slide=id.p

About Layers
תיאור מפורט כיצד Webkit מבצע את הרינדור ל Canvas - נושא שדילגתי עליו בפוסט זה.
http://www.html5rocks.com/en/tutorials/speed/layers/