ניתוח הביצועים של נתיב העיבוד הקריטי

Ilya Grigorik
Ilya Grigorik

תאריך פרסום: 31 במרץ 2014

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

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

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

עד עכשיו התמקדנו אך ורק במה שקורה בדפדפן אחרי שהמשאב (קובץ CSS,‏ JS או HTML) זמין לעיבוד. התעלמנו מהזמן שנדרש לאחזור המשאב מהמטמון או מהרשת. אנחנו נניח את הפרטים הבאים:

  • זמן הלוך ושוב ברשת (זמן האחזור להפצה) לשרת עולה 100 אלפיות השנייה.
  • זמן התגובה של השרת הוא 100 אלפיות השנייה למסמך ה-HTML ו-10 אלפיות השנייה לכל שאר הקבצים.

חוויית 'שלום עולם'

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

רוצים לנסות?

מתחילים עם רכיבי Markup בסיסיים של HTML ותמונה אחת, ללא CSS או JavaScript. לאחר מכן, פותחים את החלונית 'רשת' בכלי הפיתוח ל-Chrome ובודקים את Waterfall של המשאב שנוצר:

CRP

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

כשתוכן ה-HTML הופך לזמין, הדפדפן מנתח את הבייטים, ממיר אותם לאסימונים ובונה את עץ ה-DOM. שימו לב שכלי הפיתוח מדווחים בצורה נוחה על הזמן של האירוע DOMContentLoaded בחלק התחתון (216 אלפיות שנייה), שתואם גם לקו האנכי הכחול. הפער בין סיום ההורדה של ה-HTML לבין הקו האנכי הכחול (DOMContentLoaded) הוא הזמן שנדרש לדפדפן כדי ליצור את עץ ה-DOM – במקרה הזה, רק כמה אלפיות השנייה.

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

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

הוספת JavaScript ו-CSS למיקס

החוויה שלנו "Hello World" נראה בסיסי, אבל יש הרבה תעלומות. בפועל, לא רק קוד ה-HTML נדרש: רוב הסיכויים שיהיו לנו גיליון סגנונות של CSS וסקריפט אחד או יותר כדי להוסיף אינטראקטיביות לדף. כדאי להוסיף את שניהם כדי לראות מה קורה:

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Script</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="timing.js"></script>
  </body>
</html>

רוצים לנסות?

לפני שמוסיפים JavaScript ו-CSS:

CRP של DOM

באמצעות JavaScript ו-CSS:

DOM,‏ CSSOM,‏ JS

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

מה קרה?

  • בניגוד לדוגמה שלנו עם HTML רגיל, אנחנו צריכים גם לאחזר ולנתח את קובץ ה-CSS כדי ליצור את ה-CSSOM, ואנחנו צריכים גם את ה-DOM וגם את ה-CSSOM כדי ליצור את עץ הרינדור.
  • מכיוון שהדף מכיל גם מנתח שחוסם קובץ JavaScript, האירוע domContentLoaded ייחסם עד להורדה ולניתוח של קובץ ה-CSS: מאחר שה-JavaScript עשוי לשלוח שאילתה אל ה-CSSOM, עלינו לחסום את קובץ ה-CSS עד שהוא יורד לפני שנוכל להפעיל JavaScript.

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

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

JavaScript חיצוני:

DOM,‏ CSSOM,‏ JS

JavaScript מוטבע:

DOM,‏ CSSOM ו-JS מוטמע

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

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

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Async</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body onload="measureCRP()">
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script async src="timing.js"></script>
  </body>
</html>

רוצים לנסות?

JavaScript לחסימת ניתוח (חיצוני):

DOM, CSSOM, JS

JavaScript אסינכרוני (חיצוני):

DOM, CSSOM, JS אסינכרוני

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

לחלופין, היינו יכולים להוסיף גם את ה-CSS וגם את ה-JavaScript:

<!DOCTYPE html>
<html>
  <head>
    <title>Critical Path: Measure Inlined</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <style>
      p {
        font-weight: bold;
      }
      span {
        color: red;
      }
      p span {
        display: none;
      }
      img {
        float: right;
      }
    </style>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script>
      var span = document.getElementsByTagName('span')[0];
      span.textContent = 'interactive'; // change DOM text content
      span.style.display = 'inline'; // change CSSOM property
      // create a new element, style it, and append it to the DOM
      var loadTime = document.createElement('div');
      loadTime.textContent = 'You loaded this page on: ' + new Date();
      loadTime.style.color = 'blue';
      document.body.appendChild(loadTime);
    </script>
  </body>
</html>

רוצים לנסות?

DOM,‏ CSS מוטמע,‏ JS מוטמע

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

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

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

דפוסי ביצועים

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

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

רוצים לנסות?

Hello world CRP

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

עכשיו נבחן את אותו הדף, אבל עם קובץ CSS חיצוני:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

רוצים לנסות?

CRP + CSSOM

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

הנה כמה מונחים שבהם אנחנו משתמשים כדי לתאר את נתיב העיבוד הקריטי:

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

עכשיו אפשר להשוות את זה למאפיינים של הנתיב הקריטי בדוגמה הקודמת ל-HTML ול-CSS:

CRP + CSSOM

  • 2 משאבים קריטיים
  • 2 או יותר הלוך ושוב עבור האורך המינימלי של הנתיב הקריטי
  • 9KB של בייטים קריטיים

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

עכשיו מוסיפים לתערובת קובץ JavaScript נוסף.

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js"></script>
  </body>
</html>

רוצים לנסות?

הוספנו את app.js, שהוא נכס JavaScript חיצוני בדף וגם משאב שחוסם מנתח (כלומר, משאב קריטי). גרוע יותר, כדי להפעיל את קובץ ה-JavaScript עלינו לחסום ולהמתין ל-CSSOM. נזכיר ש-JavaScript יכול לשלוח שאילתה ל-CSSOM, ולכן הדפדפן מושהה עד להורדת style.css ו-CSSOM.

DOM, CSSOM, JavaScript CRP

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

  • 3 משאבים קריטיים
  • 2 או יותר הלוך ושוב עבור האורך המינימלי של הנתיב הקריטי
  • 11KB של בייטים קריטיים

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

אחרי שיחה עם מפתחי האתר שלנו, הבנו שקוד ה-JavaScript שכללנו בדף לא צריך לחסום אותו. יש לנו שם ניתוח נתונים וקוד אחר שלא צריך לחסום את העיבוד של הדף. בעזרת המידע הזה, אפשר להוסיף את המאפיין async לאלמנט <script> כדי לבטל את החסימה של המנתח:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

רוצים לנסות?

DOM, CSSOM, CRP אסינכרוני של JavaScript

לסקריפט אסינכרוני יש כמה יתרונות:

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

כתוצאה מכך, הדף המותאם עכשיו חוזר לשני משאבים קריטיים (HTML ו-CSS), עם אורך נתיב קריטי מינימלי של שתי נסיעות הלוך ושוב, ו-9KB בסך הכול של בייטים קריטיים.

לסיום, אם גיליון הסגנונות של ה-CSS היה נדרש רק לדפוס, איך הוא היה נראה?

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" media="print" />
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
    <script src="app.js" async></script>
  </body>
</html>

רוצים לנסות?

DOM,‏ CSS לא חוסם ו-CRP של JavaScript לא סנכרוני

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

משוב