شناسایی و رفع تنگناهای عملکرد مسیر رندر حیاتی مستلزم دانش خوب از مشکلات رایج است. بیایید یک تور عملی داشته باشیم و الگوهای عملکرد رایج را استخراج کنیم که به شما در بهینه سازی صفحاتتان کمک می کند.
بهینهسازی مسیر رندر حیاتی به مرورگر اجازه میدهد تا صفحه را در سریعترین زمان ممکن نقاشی کند: صفحات سریعتر به تعامل بالاتر، صفحات بیشتر مشاهده شده و تبدیل بهبود یافته تبدیل میشوند . برای به حداقل رساندن مدت زمانی که یک بازدیدکننده صرف مشاهده یک صفحه خالی می کند، باید بهینه سازی کنیم که کدام منابع و به ترتیب بارگذاری می شوند.
برای کمک به تشریح این فرآیند، بیایید با سادهترین حالت ممکن شروع کنیم و صفحه خود را بهصورت تدریجی بسازیم تا منابع، سبکها و منطق برنامههای اضافی را شامل شود. در این فرآیند، ما هر مورد را بهینه خواهیم کرد. ما همچنین خواهیم دید که در کجا ممکن است همه چیز خراب شود.
تا کنون ما به طور انحصاری روی آنچه در مرورگر پس از در دسترس قرار گرفتن منبع (فایل CSS، JS یا HTML) برای پردازش اتفاق میافتد، تمرکز کردهایم. ما زمان لازم برای واکشی منبع از حافظه پنهان یا از شبکه را نادیده گرفته ایم. موارد زیر را فرض می کنیم:
- هزینه رفت و برگشت شبکه (تأخیر انتشار) به سرور 100 میلی ثانیه است.
- زمان پاسخگویی سرور برای سند HTML 100 میلیثانیه و برای همه فایلهای دیگر 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 یا جاوا اسکریپت. بیایید جدول زمانی شبکه خود را در Chrome DevTools باز کنیم و آبشار منبع حاصل را بررسی کنیم:
همانطور که انتظار می رفت، دانلود فایل HTML تقریباً 200 میلی ثانیه طول کشید. توجه داشته باشید که قسمت شفاف خط آبی نشان دهنده مدت زمانی است که مرورگر بدون دریافت هیچ بایت پاسخی در شبکه منتظر می ماند در حالی که بخش جامد زمان پایان دانلود را پس از دریافت اولین بایت پاسخ نشان می دهد. دانلود HTML بسیار کوچک است (<4K)، بنابراین تنها چیزی که ما نیاز داریم یک رفت و برگشت برای واکشی فایل کامل است. در نتیجه، واکشی سند HTML تقریباً 200 میلیثانیه طول میکشد که نیمی از زمان انتظار در شبکه و نیمی دیگر در انتظار پاسخ سرور است.
هنگامی که محتوای HTML در دسترس می شود، مرورگر بایت ها را تجزیه می کند، آنها را به توکن تبدیل می کند و درخت DOM را می سازد. توجه داشته باشید که DevTools به راحتی زمان رویداد DOMContentLoaded را در پایین (216 میلیثانیه) گزارش میکند که با خط عمودی آبی نیز مطابقت دارد. فاصله بین پایان دانلود HTML و خط عمودی آبی (DOMContentLoaded) زمانی است که مرورگر برای ساختن درخت DOM طول میکشد — در این مورد، فقط چند میلی ثانیه.
توجه داشته باشید که "عکس عالی" ما رویداد domContentLoaded
را مسدود نکرد. به نظر می رسد، ما می توانیم درخت رندر را بسازیم و حتی صفحه را بدون منتظر ماندن برای هر دارایی در صفحه نقاشی کنیم: همه منابع برای ارائه اولین رنگ سریع حیاتی نیستند . در واقع، وقتی در مورد مسیر رندر بحرانی صحبت می کنیم، معمولاً در مورد نشانه گذاری HTML، CSS و جاوا اسکریپت صحبت می کنیم. تصاویر رندر اولیه صفحه را مسدود نمیکنند - اگرچه ما باید تلاش کنیم تا تصاویر را در اسرع وقت نقاشی کنیم.
گفته شد، رویداد load
(همچنین به عنوان onload
شناخته میشود)، روی تصویر مسدود شده است: DevTools رویداد onload
را با سرعت 335 میلیثانیه گزارش میکند. به یاد داشته باشید که رویداد onload
نقطه ای را مشخص می کند که در آن تمام منابع مورد نیاز صفحه دانلود و پردازش شده اند. در این مرحله، اسپینر بارگیری می تواند چرخش را در مرورگر متوقف کند (خط عمودی قرمز در آبشار).
افزودن جاوا اسکریپت و 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>
قبل از افزودن جاوا اسکریپت و CSS:
با جاوا اسکریپت و CSS:
افزودن فایلهای CSS و جاوا اسکریپت خارجی دو درخواست اضافی را به آبشار ما اضافه میکند که مرورگر همه آنها را تقریباً همزمان ارسال میکند. با این حال، توجه داشته باشید که اکنون تفاوت زمانی بسیار کمتری بین رویدادهای domContentLoaded
و onload
وجود دارد.
چه اتفاقی افتاد؟
- برخلاف مثال ساده HTML ما، ما همچنین باید فایل CSS را برای ساختن CSSOM واکشی و تجزیه کنیم، و برای ساختن درخت رندر به هر دو DOM و CSSOM نیاز داریم.
- از آنجایی که صفحه حاوی یک تجزیه کننده فایل جاوا اسکریپت مسدود کننده است، رویداد
domContentLoaded
تا زمانی که فایل CSS دانلود و تجزیه نشود مسدود می شود: چون جاوا اسکریپت ممکن است CSSOM را پرس و جو کند، ما باید فایل CSS را تا زمانی که دانلود شود مسدود کنیم تا بتوانیم جاوا اسکریپت را اجرا کنیم.
اگر اسکریپت خارجی خود را با یک اسکریپت درون خطی جایگزین کنیم چه؟ حتی اگر اسکریپت مستقیماً داخل صفحه باشد، مرورگر نمیتواند آن را تا زمانی که CSSOM ساخته شود اجرا کند. به طور خلاصه، جاوا اسکریپت درون خطی نیز مسدودکننده تجزیه کننده است.
گفتنی است، با وجود مسدود شدن در CSS، آیا درونخطسازی اسکریپت باعث میشود صفحه سریعتر رندر شود؟ بیایید آن را امتحان کنیم و ببینیم چه می شود.
جاوا اسکریپت خارجی:
جاوا اسکریپت درون خطی:
ما یک درخواست کمتر ارائه می کنیم، اما هر دو زمان onload
و domContentLoaded
ما عملاً یکسان هستند. چرا؟ خوب، ما می دانیم که مهم نیست جاوا اسکریپت درون خطی یا خارجی باشد، زیرا به محض اینکه مرورگر تگ اسکریپت را زد، آن را مسدود می کند و منتظر می ماند تا CSSOM ساخته شود. علاوه بر این، در مثال اول ما، مرورگر هر دو CSS و جاوا اسکریپت را به صورت موازی دانلود میکند و تقریباً همزمان دانلود میشوند. در این مثال، داخل کردن کد جاوا اسکریپت کمک زیادی به ما نمی کند. اما چندین استراتژی وجود دارد که می تواند صفحه ما را سریعتر رندر کند.
ابتدا به یاد بیاورید که همه اسکریپت های درون خطی مسدود کننده تجزیه کننده هستند، اما برای اسکریپت های خارجی می توانیم کلمه کلیدی "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>
جاوا اسکریپت مسدودکننده تجزیه کننده (خارجی):
جاوا اسکریپت غیرهمگام (خارجی):
خیلی بهتر! رویداد domContentLoaded
مدت کوتاهی پس از تجزیه HTML فعال می شود. مرورگر می داند که در جاوا اسکریپت مسدود نمی شود و از آنجایی که هیچ اسکریپت مسدودکننده تجزیه کننده دیگری وجود ندارد، ساخت CSSOM نیز می تواند به صورت موازی ادامه یابد.
از طرف دیگر، ما میتوانستیم هم CSS و هم جاوا اسکریپت را درون خطی کنیم:
<!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
عملاً مانند مثال قبلی است. بهجای علامتگذاری جاوا اسکریپت بهعنوان غیر همگام، CSS و JS را در خود صفحه قرار دادهایم. این باعث میشود صفحه HTML ما بسیار بزرگتر شود، اما نکته مثبت این است که مرورگر برای دریافت منابع خارجی نیازی به صبر ندارد. همه چیز همانجا در صفحه است.
همانطور که می بینید، حتی با یک صفحه بسیار ساده، بهینه سازی مسیر رندر بحرانی یک تمرین غیر ضروری است: ما باید نمودار وابستگی بین منابع مختلف را درک کنیم، باید شناسایی کنیم که کدام منابع "بحرانی" هستند و باید انتخاب کنیم. در میان استراتژی های مختلف برای نحوه گنجاندن آن منابع در صفحه. هیچ راه حلی برای این مشکل وجود ندارد. هر صفحه متفاوت است برای تعیین استراتژی بهینه باید فرآیند مشابهی را به تنهایی دنبال کنید.
با این اوصاف، بیایید ببینیم آیا میتوانیم به عقب برگردیم و برخی از الگوهای عملکرد کلی را شناسایی کنیم.
الگوهای عملکرد
ساده ترین صفحه ممکن فقط از نشانه گذاری HTML تشکیل شده است. بدون CSS، بدون جاوا اسکریپت یا انواع دیگر منابع. برای رندر کردن این صفحه، مرورگر باید درخواست را آغاز کند، صبر کند تا سند 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>
زمان بین T 0 و T 1 زمان پردازش شبکه و سرور را ثبت می کند. در بهترین حالت (اگر فایل 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 یا بیشتر رفت و برگشت برای حداقل طول مسیر بحرانی
- 9 کیلوبایت بایت بحرانی
برای ساختن درخت رندر به هر دو HTML و CSS نیاز داریم. در نتیجه، هر دو HTML و CSS منابع حیاتی هستند: CSS تنها پس از دریافت سند HTML توسط مرورگر واکشی می شود، بنابراین طول مسیر بحرانی حداقل دو رفت و برگشت است. هر دو منبع در مجموع به 9 کیلوبایت بایت بحرانی میرسند.
حالا بیایید یک فایل جاوا اسکریپت اضافی را به ترکیب اضافه کنیم.
<!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
اضافه کردیم که هم یک دارایی جاوا اسکریپت خارجی در صفحه و هم یک منبع مسدودکننده تجزیه کننده (یعنی حیاتی) است. بدتر از آن، برای اجرای فایل جاوا اسکریپت باید مسدود کنیم و منتظر CSSOM باشیم. به یاد بیاورید که جاوا اسکریپت می تواند CSSOM را پرس و جو کند و از این رو مرورگر مکث می کند تا style.css
دانلود شود و CSSOM ساخته شود.
در عمل، اگر به "شبکه آبشار" این صفحه نگاه کنیم، خواهید دید که هر دو درخواست CSS و جاوا اسکریپت تقریباً در یک زمان آغاز می شوند. مرورگر HTML را دریافت می کند، هر دو منبع را کشف می کند و هر دو درخواست را آغاز می کند. در نتیجه، صفحه فوق دارای ویژگی های مسیر بحرانی زیر است:
- 3 منابع حیاتی
- 2 یا بیشتر رفت و برگشت برای حداقل طول مسیر بحرانی
- 11 کیلوبایت بایت بحرانی
ما اکنون سه منبع حیاتی داریم که تا 11 کیلوبایت بایت بحرانی اضافه می کنند، اما طول مسیر بحرانی ما هنوز دو رفت و برگشت است زیرا می توانیم CSS و جاوا اسکریپت را به صورت موازی انتقال دهیم. مشخص کردن ویژگیهای مسیر رندر بحرانی شما به این معنی است که میتوانید منابع حیاتی را شناسایی کنید و همچنین درک کنید که مرورگر چگونه واکشی آنها را زمانبندی میکند. بیایید به مثال خود ادامه دهیم.
پس از گپ زدن با توسعه دهندگان سایت، متوجه می شویم که جاوا اسکریپتی که در صفحه خود قرار داده ایم نیازی به مسدود شدن ندارد. ما مقداری تجزیه و تحلیل و کدهای دیگر در آنجا داریم که نیازی به مسدود کردن رندر صفحه ما ندارد. با این دانش، میتوانیم ویژگی «async» را به تگ اسکریپت اضافه کنیم تا تجزیهکننده را رفع انسداد کنیم:
<!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>
یک اسکریپت ناهمزمان چندین مزیت دارد:
- اسکریپت دیگر مسدود کننده تجزیه کننده نیست و بخشی از مسیر رندر حیاتی نیست.
- از آنجا که هیچ اسکریپت مهم دیگری وجود ندارد، CSS نیازی به مسدود کردن رویداد
domContentLoaded
ندارد. - هر چه زودتر رویداد
domContentLoaded
فعال شود، منطق برنامه دیگر زودتر شروع به اجرا می کند.
در نتیجه، صفحه بهینه شده ما اکنون به دو منبع حیاتی (HTML و CSS) با حداقل طول مسیر بحرانی دو رفت و برگشت و مجموعا 9 کیلوبایت بایت بحرانی بازگشته است.
در نهایت، اگر شیوه نامه 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) و حداقل طول مسیر رندر بحرانی یک رفت و برگشت است.