תאריך פרסום: 31 במרץ 2014
כדי לזהות ולפתור צווארי בקבוק קריטיים בביצועים של נתיב העיבוד, צריך להכיר היטב את החסרונות הנפוצים. סיור מודרך לזיהוי דפוסי ביצועים נפוצים יעזור לכם לבצע אופטימיזציה של הדפים.
אופטימיזציה של נתיב העיבוד הקריטי מאפשרת לדפדפן לצייר את הדף מהר ככל האפשר: דפים מהירים יותר מביאים למעורבות גבוהה יותר, להצגה של יותר דפים ולשיפור ההמרות. כדי לקצר את משך הזמן שבו המבקרים צופים במסך ריק, אנחנו צריכים לבצע אופטימיזציה של המשאבים שאנחנו טוענים ובאיזה סדר.
כדי להמחיש את התהליך הזה, נתחיל מהמקרה הפשוט ביותר ונמשיך בהדרגה להוסיף לדף משאבים, סגנונות ולוגיקה של אפליקציה. במהלך התהליך, נבצע אופטימיזציה של כל מקרה, וגם נבדוק איפה הדברים עלולים להשתבש.
עד עכשיו התמקדנו אך ורק במה שקורה בדפדפן אחרי שהמשאב (קובץ CSS, JS או HTML) זמין לעיבוד. התעלמנו מהזמן שנדרש לאחזור המשאב מהמטמון או מהרשת. נניח את הפרטים הבאים:
- זמן הלוך ושוב ברשת (זמן האחזור להפצה) לשרת עולה 100 אלפיות השנייה.
- זמן התגובה של השרת הוא 100 אלפיות השנייה למסמך ה-HTML ו-10 אלפיות השנייה לכל שאר הקבצים.
חוויית hello world
<!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 ובודקים את רשימת המשאבים שמופיעה:
כצפוי, ההורדה של קובץ ה-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 ו-script אחד או יותר כדי להוסיף אינטראקטיביות לדף. מוסיפים את שניהם למיקס כדי לראות מה קורה:
<!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:
באמצעות JavaScript ו-CSS:
הוספת קובצי CSS ו-JavaScript חיצוניים מוסיפה שתי בקשות נוספות לרשימת הרשתות בתהליך בחירת הרשת, והדפדפן שולח את כולן בערך באותו הזמן. עם זאת, חשוב לזכור שיש עכשיו הבדל קטן בהרבה בין התזמון של האירועים domContentLoaded
ו-onload
.
מה קרה?
- בניגוד לדוגמה שלנו עם HTML פשוט, אנחנו צריכים גם לאחזר ולנתח את קובץ ה-CSS כדי ליצור את ה-CSSOM, ואנחנו צריכים גם את ה-DOM וגם את ה-CSSOM כדי ליצור את עץ הרינדור.
- מכיוון שהדף מכיל גם קובץ JavaScript לחסימת מנתח, האירוע
domContentLoaded
חסום עד להורדה ולניתוח של קובץ ה-CSS: מאחר ש-JavaScript עשוי לשלוח שאילתה ל-CSSOM, אנחנו חייבים לחסום את קובץ ה-CSS עד להורדה שלו כדי שנוכל להריץ את JavaScript.
מה קורה אם מחליפים את הסקריפט החיצוני בסקריפט מוטמע? גם אם הסקריפט מוטמע ישירות בדף, הדפדפן לא יכול להריץ אותו עד ליצירת ה-CSSOM. בקיצור, JavaScript מוטמע הוא גם חסימה של מנתח.
עם זאת, למרות החסימה ב-CSS, האם הטמעת הסקריפט בקוד מזרזת את העיבוד של הדף? כדאי לנסות ולראות מה קורה.
JavaScript חיצוני:
JavaScript מוטמע:
אנחנו שולחים בקשה אחת פחות, אבל זמני 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 (חיצוני) שחוסם את המנתח:
JavaScript אסינכרוני (חיצוני):
הרבה יותר טוב! האירוע 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>
שימו לב שהזמן 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>
הזמן שבין 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>
שוב, אנחנו צריכים לבצע נסיעה הלוך ושוב ברשת כדי לאחזר את מסמך ה-HTML, ואז התגים המאוחזרים מורים לנו שאנחנו צריכים גם את קובץ ה-CSS. כלומר, הדפדפן צריך לחזור לשרת ולקבל את ה-CSS לפני שהוא יכול להציג את הדף במסך. כתוצאה מכך, הדף הזה כרוך בשני סבבי נסיעות לפחות לפני שהוא יכול להופיע. שוב, יכול להיות שתהליך ההעברה של קובץ ה-CSS יכלול כמה נסיעות הלוך ושוב, ולכן הדגש הוא על 'מינימום'.
ריכזנו כאן כמה מונחים שבהם אנחנו משתמשים כדי לתאר את נתיב העיבוד הקריטי:
- משאב קריטי: משאב שעלול לחסום את העיבוד הראשוני של הדף.
- אורך הנתיב הקריטי: מספר הנסיעות הלוך ושוב, או משך הזמן הכולל שנדרש לאחזור כל המשאבים הקריטיים.
- בייטים קריטיים: המספר הכולל של הבייטים הנדרשים כדי להגיע לעיבוד הגרפי הראשון של הדף, שהוא הסכום של גודל קובץ ההעברה של כל המשאבים הקריטיים. בדוגמה הראשונה שלנו, עם דף HTML אחד, היה משאב קריטי אחד (מסמך ה-HTML). אורך הנתיב הקריטי היה שווה גם למסלול הלוך ושוב אחד ברשת (בהנחה שהקובץ היה קטן), והבייטים הקריטיים הכוללים היו רק גודל ההעברה של מסמך ה-HTML עצמו.
עכשיו אפשר להשוות את זה למאפיינים של הנתיב הקריטי בדוגמה הקודמת ל-HTML ול-CSS:
- 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 ייוצר.
עם זאת, בפועל, אם נסתכל על 'מפל הרשת' של הדף הזה, נראה שגם הבקשות ל-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>
לסקריפט אסינכרוני יש כמה יתרונות:
- הסקריפט כבר לא חוסם את המנתח והוא לא חלק מנתיב העיבוד הקריטי.
- מכיוון שאין סקריפטים קריטיים אחרים, אין צורך לחסום את האירוע
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>
מכיוון שהמשאב style.css משמש רק להדפסה, הדפדפן לא צריך לחסום אותו כדי להציג את הדף. לכן, ברגע שהבנייה של DOM מסתיימת, בדפדפן יש מספיק מידע כדי להציג את הדף. כתוצאה מכך, לדף הזה יש רק משאב קריטי אחד (מסמך ה-HTML), ואורך נתיב העיבוד הקריטי המינימלי הוא נסיעה הלוך ושוב אחת.