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

Ilya Grigorik
Ilya Grigorik

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

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

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

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

הוספת JavaScript ו-CSS לשילוב

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

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

<!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, async 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>

רוצים לנסות?

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

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

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

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

DOM + CSSOM CRP

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

כדי ליצור את עץ העיבוד, אנחנו זקוקים גם ל-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

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

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

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

לאחר שיחת צ'אט עם מפתחי האתרים שלנו, אנחנו מבינים שאין צורך לחסום את 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" async></script>
  </body>
</html>

רוצים לנסות?

DOM, CSSOM, async JavaScript CRP

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

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

משוב