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

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>

רוצים לנסות?

התחל עם תגי עיצוב בסיסיים של HTML ותמונה אחת, ללא CSS או JavaScript. לאחר מכן, פותחים את החלונית Network (רשת) ב-Chrome DevTools ובודקים את רשימת המשאבים שמופיעה:

CRP

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

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

DOM CRP

באמצעות 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 כאסינכרוני, הוספנו את ה-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>

רוצים לנסות?

DOM + CSSOM CRP

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

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

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

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

DOM + CSSOM CRP

  • 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 לא סנכרוני

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

  • הסקריפט כבר לא חוסם את המנתח והוא לא חלק מנתיב העיבוד הקריטי.
  • מכיוון שאין סקריפטים קריטיים אחרים, אין צורך לחסום את האירוע domContentLoaded באמצעות ה-CSS.
  • ככל שהאירוע 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), ואורך נתיב העיבוד הקריטי המינימלי הוא נסיעה הלוך ושוב אחת.

משוב