آیا تا به حال برای شما سوال شده است که چرا صفحات جستجوی گوگل یا وبسایت آمازون اینقدر سریع بارگذاری میشوند؟ خب، همراه من باشید تا این مفهوم که به شدت کارایی صفحه را ارتقا میدهد را برای شما بررسی کنم. اما در ابتدا، بیایید برخی مفاهیم را بررسی کنیم که به این ایده ختم میشوند.
تجزیه و تحلیل مسیر رندر کردن حیاتی (CRP = Critical Rendering Path)
اول از همه بیاید لغاتی که استفاده خواهیم کرد را تعریف کنیم:
۱. منبع حیاتی (Critical Resource): منبعی که میتواند رندر کردن اولیه صفحه را مسدود کند.
۲. زمان اولین بایت (Time To First Byte (TTFB)): مدت زمان را از زمانی که مرورگر یک درخواست HTTP را ارسال میکند، تا زمانی که بایت صفحه توسط مرورگر دریافت میشود، اندازه گیری میکند.
بهینهسازی کارایی وب، تماما درباره درک این است که چه اتفاقی در قدمهای حد واسط بین دریافت فایلهای HTML، CSS و JavaScript، و پردازش مورد نیاز برای تبدیل آنها به پیکسلهای رندر شده میافتد. این، «مسیر رندر کردن حیاتی» (CRP) میباشد.
قبل از این که صفحات رندر شوند، مرورگر باید این قدمها را بگذراند:
وقتی که مرورگر در ابتدا با صفحه برخورد میکند، فایل HTML را دریافت میکند. سپس هم شروع به ساختن درخت DOM (مدل آبجکت سند = Document Object Model) مینماید. هر تگ داخل HTML، نمایانگر یک گره داخل درخت DOM میباشد که تمام اطلاعات مربوطه را دارد. بیایید از یک مثال استفاده کنیم، تا بتوانیم این مسئله را درک کنیم.
فرض کنید که مرورگر این HTML را از سرور دریافت میکند:
<html>
<head>
<meta name="viewport" content="width=device-width,initial- scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
مرورگر آن را به یک آبجکت به نام DOM، به این صورت تبدیل میکند:
نکته: روند ساخت و ساز DOM، افزایشی است. این اساس ایدهای است که من این مقاله را برایش مینویسم.
در حالیکه مرورگر در حال ساخت و ساز DOM بود، با یک تگ link در بخش head مواجه شد که به یک استایلشیت CSS ارجاع میکرد.
مرورگر با پیشبینی این که بعدا به این منبع برای رندر کردن صفحه نیاز خواهد داشت، درخواستی را برای آن ارسال میکند که با این محتویات باز میگردد:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
سپس مرورگر CSSOM (مدل آبجکت CSS = CSS Object Model) را میسازد:
درختهای CSSOM و DOM ترکیب شدهاند، تا یک درخت رندر را تشکیل دهند. سپس درخت رندر برای محاسبه طرح هر عنصر قابل رویت استفاده میشود.
ظاهر یک درخت رندر، به این صورت است:
برخی گرهها (ماننند تگهای script و meta) قابل رویت نیستند و از آنجایی که در خروجی رندر شده منعکس نشدهاند، حذف شدهاند. برخی گرهها توسط CSS مخفی شدهاند و همچنین از درخت رندر حذف شدهاند.
حال که درخت رندر در جای خود قرار دارد، میتوانیم به قدم طرح (Layout) برویم. خروجی روند طرح، یک مدل جعبه (Box Model) است که در آن موقعیت و اندازه دقیق هر عنصر قرار داده میشود. تمام این اندازهگیریهای نسبی، بر روی صفحه تبدیل به پیکسلهای مطلق میشوند.
در آخر، حال که میدانیم کدام گرهها قابل رویت هستند، و همچنین استایلها و موقعیتهای محاسبه شدهشان را نیز میدانیم، میتوانیم اطلاعات را به مرحله نهایی منتقل کنیم. این مرحل، هر گره موجود در درخت رندر را به پیکسلهای واقعی بر روی صفحه تبدیل میکند. این قدم معمولا با نام «رنگآمیزی» شناخته میشود.
نکته: CSS، رندر را مسدود میکند. تا زمانی که CSSOM ساخته شود، مرورگر نمیتواند قدم درخت رندر را پردازش کند. از این رو، باید فایلهای CSS را در اولین فرصت به مرورگر تحویل دهیم، که به همن علت تمام تگهای link را در بخش head نگه میداریم.
حال بیایید JavaScript را به مثال خود اضافه کنیم:
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js"></script>
</body>
به طور پیشفرض، اجرای JavaScript، parser را مسدود میکند. وقتی که مرورگر با یک تگ script در سند مواجه میشود، این قدمها را دنبال میکند:
۱. توقف ساخت و ساز DOM
۲. دانلود فایل
۳. انتقال کنترل به رانش JavaScript
۴. قبل از ادامه دادن با ساخت و ساز DOM، اجازه بده اسکریت اجرا شود.
مرورگر نمیداند که اسکریپت چه کاری میخواهد بر روی صفحه انجام دهد؛ پس بدترین حالت را تصور میکند و parser را مسدود میکند.
صبر کنید! این بدترین حالی نیست که میتواند در هنگام parse کردن DOM اتفاق بیفتد. در مثال آخر، میتوانیم ببینیم که هر دو فایل خارجی CSS و JavaScript که مرورگر نیاز دارد تا دانلود کند را داریم.
حال، فرض کنید که فایل CSS مقداری زمان میبرد تا دانلود شود، و در همین حین فایل JavaScript دانلود میشود. حال، مرورگر بدترین حالتی که JavaScript ممکن است CCSOM را کوئری کند را فرض خواهد کرد، که به همین علت parse کردن فایل JavaScript را تا زمانی که فایل CSS دانلود شده است و CCSOM آماده میباشد، شروع نمیکند.
بیایید به نموداری نگاه داشته باشیم که میتواند به ما کمک کند تا درک بهتری از منظور من را داشته باشیم:
CSS برای هر صفحه وبی یک شیطان است! CSS، رندر و parse کردن را مسدود میکند. ما باید در مدیریت آن بسیار مراقب باشیم.
بیایید به برخی راهها برای بهینهسازی CRP نگاه داشته باشیم.
بهینهسازی CPR
تا به اینجا، میدانیم که CSS میتواند یک شیطان باشد. برای بهینهسازی زمان اولین رندر، در اولین فرصت آن را به کلاینت برسانید. اگر ما برخی استایلهای CSS را داریم که فقط در شرایطی خاص استفاده میشوند چه؟ برای مثال، وقتی که صفحه در حال چاپ شدن، یا نمایش داده شدن بر روی یک صفحه بزرگ است؟
اگر مجبور نبودیم که رندر کردن را در این منابع مسدود کنیم، خیلی خوب میشد. «نوع رسانه» (Media types) و «صفهای رسانه» (Media queries) در CSS ما را قادر میسازند تا به این موارد خطاب کنیم:
<link href="style.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 40em)">
یک صف رسانه از یک نوع رسانه تشکیل میشود که وضعیت برخی امکانات خاص رسانه را بررسی میکند. برای مثال، اولین اعلامیه استایلشیت ما هیچ نوع یا صف رسانهای را فراهم نمیکند، پس به تمام موارد اعمال میشود. به عبارتی دیگر، همیشه رندر کردن مسدود میشود.
در سمت دیگر، اعلامیه دوم استایلشیت فقط وقتی که محتویات در حال چاپ شدن هستند، اعمال میشود. از این رو نیاز نیست وقتی که صفحه در ابتدا بارگذاری میشود، اعلامیه استایلشیت، رندر کردن آن را مسدود کند.
در آخر، اگر شرایط تطابق داشته باشند، آخرین استایلشیت توسط مرورگر اجرا خواهد شد. اگر شروط مورد نظر تطابق نداشته باشند، مرورگر رندر کردن را مسدود نخواهد کرد.
وقتی که داراییهای استایلشیت خود را اعلام میکنید، توجه زیادی به نوع و صف رسانه داشته باشید. این موارد تاثیر زیادی بر روی کارایی مسیر رندر کردن حیاتی دارند.
کل JavaScript به طور پیشفرض، parser را مسدود میکند. ارسال یک سیگنال به مرورگر درباره این که نیازی نیست اسکریپت در جایی که ارجاع شده است، اجرا شود، مرورگر را قادر میسازد تا DOM را بسازد و وقتی که اسکریپت آماده باشد، اجازه میدهد که اجرا شود. برای مثال، پس از این که فایل مورد نظر از کش یا سرور کنترل از راه دور دریافت شده است.
برای رسیدن به این هدف ما script خود را به عنوان async نشانهگذاری میکنیم:
<script src=”app.js” async></script>
اضافه کردن کلمه کلیدی async به تگ script، به مرورگر میگوید که در حالیکه منتظر است تا اسکریپت در دسترس قرار بگیرد، ساخت و ساز DOM را متوقف نکند که به طور قابل توجهی کارایی را ارتقا میدهد.
یک نقطه قوت دیگر از صفت async این است که در حالیکه script منتظر است تا CSSOM حاضر شود، مسدود نمیشود.
با توجه به این که script به هیچ صورتی DOM را تغییر نمیدهد، اسکریپت تجزیه و تحلیل یک مثال خوب برایی صفت async است.
و در نهایت، نقطه اوج مقاله فرا میرسد، که در آن راز اصلی که شرکتها از آن استفاده میکنند (جدا از بهینهسازیای که در بالا به آن اشاره شد) را به شما میگویم.
ارسال HTML به صورت قطعه قطعه از سرور
به تصویر زیر نگاه کنید و تصمیم بگیرید: چگونه میخواهید وبسایتتان بارگذاری شود؟
پاسخ خود را به دست آوردید؟ قطعا مورد اول! هیچ کس دوست ندارد که برای مدت طولانی یک صفحه خالی ببیند. بسیار بهتر است که HTML را به صورت قطعه قطعه بر روی صفحه رندر کنیم، که صفحه جستجوی گوگل، آمازون و غولهای دیگر هم همین کار را انجام میدهند.
حال وقتی که در ابتدا بر روی URL صفحه کلیک میکنید، HTML کامل صفحه بر روی سرور ساخته میشود. تا آن زمان، مرورگر بیکار مینشیند و کاری انجام نمیدهد.
پس از این که HTML بر روی سرور ساخته شده است، به مرورگر منتقل میشود. سپس مرورگر شروع به ساخت DOM مینماید و تمام قدمهای CRP را همانطور که پیشتر به آنها اشاره شد، میگذارند.
نمودار زیر، به ما کمک خواهد کرد که این مسئله را بهتر درک کنیم:
پس چرا ما وقت بیکاری مرورگر را بهینهسازی نکنیم و آن را مجبور نکنیم که با ارسال قطعههای HTML به سرور، ساخت DOM را شروع کند؟ به زبانی دیگر، به جای این که صبر کنیم کل HTML آماده شود، میتوانیم به محض این که قطعههای HTML آماده شدند، آن را به همان صورت قطعه قطعه ارسال کنیم. این کار باعث میشود که مرورگر به جای بیکار ماندن، شروع به ساخت درخت DOM / CSSOM کند. این یک ایده عالی است!
امیدوارم که این نمودار هم در درک بهتر مسئله به شما کمک کند:
صفحه به قطعههای HTML بر روی سرور تقسیم شده است. حال سرور به جای این که منتظر بماند تا کل HTML آماده شود و سپس آن را برای مرورگر ارسال کند، آن را به صورت قطعه قطعه و به محض این که بر روی سرور آماده شد، ارسال میکند. این یعنی اولین قطعه منتظر نمیماند تا دو قطعه دیگر آماده شوند؛ بلکه به محض این که قطعهها بر روی سرور آماده شوند، آنها را به مرورگر ارسال میکند.
بیایید از مثالی استفاده کنیم تا حتی بهتر این مسئله را درک کنیم. عکس زیر، صفحه جستجوی گوگل است:
حال فرض کنید که بر روی این URL کلیک کنیم، و مرورگر درخواستی را به سرور ارسال کند تا این صفحه را دریافت کند. سرور شروع به ساخت این صفحه کرده و بخش اول HTML را به اتمام رسانده است، اما برای بخش دوم باید مقدار داده دریافت کند که کمی زمان خواهد برد.
حال، سرور به جای این که منتظر بماند تا بخش دوم کامل شود، بخش اول را به مرورگر ارسال میکند تا مرورگر شروع به ساخت DOM نماید.
در همین حین، سرور HTML بخش دوم را با دادههای مورد نیاز، آماده میکند. به این صورت، کاربر خواهد توانست تا ببیند که صفحه وب به تدریج بر روی صفحه بارگذاری میشود. ارسال قطعههای HTML، همچنین «زمان اولین بایت» را نیز کاهش میدهد و کارایی و سرعت صفحه را ارتقا میدهد.
این دقیقا کاری است که گوگل بر روی صفحات خود انجام میدهد! حتی آمازون هم در حالیکه باقی صفحه در حال آماده شدن بر روی سرور است، header خود را ارسال میکند.
ارسال HTML به صورت قطعه قطعه، همچنین یک منفعت دیگر هم دارد. به محض این که تگ head شما به کلاینت میرسد، مرورگر CSS و باقی درخواستهای موجود در تگ را راهاندازی میکند. این مسئله به مرورگر کمک میکند تا در حالیکه باقی HTML توسط سرور آماده میشود، منابع حیاتی را دانلود کند.
زمان عادی برای دریافت یک صفحه از سرور، حدود ۵۰۰ میلی ثانیه است. اما زمان عادی برای دریافت قطعه اول از سرور، حدود ۲۰ تا ۳۰ میلی ثانیه است. فراخوانی CSS که قرار بود پس از ۵۰۰ میلی ثانیه راهاندازی شود، حال پس از ۲۰ تا ۳۰ میلی ثانیه راهاندازی خواهد شد، که حدود ۴۷۰ تا ۴۸۰ ثانیه صفحه شما را سریعتر میکند. شما حتی میتوانید تصاویر سنگین را در تگ head، preload کنید.
حال سوال این است که: چگونه HTML را به صورت قطعه قطعه از سمت سرور ارسال کنیم؟
خب، ما راههای مختلفی در زبانهای مختلف داریم. ما متدی به نام flush در Java، .NET و PHP داریم. در Node.js، باید هر زمان که قطعههای HTML ما آمادهاند، آنها را res.write() کنیم.
نکته: مرورگر برای این که تمام قطعهها را به دست بیاورد، فراخوانیهای تکراری را به سرور ارسال نمیکند؛ بلکه تمام قطعههای HTML توسط یک درخواست تکی به سرور ارسال میشوند.
POC من
من یک POC با استفاده از Node.js، Express و React ساختهام، که کامپوننتهای React در آن بر روی Node.js رندر شدهاند و هر کامپوننت به محض این که HTML آن حاضر شده است، به مرورگر ارسال میشود. شما میتوانید سورس کد آن را در این لینک پیدا کنید.
همچنین دموی آن را میتوانید بر روی این لینک مشاهده کنید.
در این دمو، میتوانید چند لینک مشاهده کنید. لینک «Move to page without chunking» شما را به صفحهای میبرد که مفهوم قطعه قطعه در آن به کار برده نشده است. لینک «Move to page with chunking» هم شما را به صفحهای میبرد که مفهوم قطعه قطعه در آن به کار برده شده است. در اینجا، اسکرینشاتهایی از این صفحه را مشاهده میکنید.
این صفحه به ۴ بخش تقسیم شده است. به محض این که بخش اول بر روی سرور آماده شود، به مرورگر ارسال میشود تا مرورگر بتواند شروع به ساخت DOM نماید.
بخش دوم با استفاده از دادههای یک API ساخته شده است که کمی زمان خواهد برد. تا آن زمان، مرورگر HTML مربوط به بخش اول را به عنوان یک ساخت و ساز DOM، در یک روند افزایشی میسازد.
به محض این که HTML بخش دوم بر روی سرور آماده شود، به مرورگر تحویل داده میشود. این روال برای بخش سوم و چهارم هم ادامه دارد.
در اینجا یک منفعت وجود دارد: حتی قبل از فرستادن بخش اول، من یک قطعه دیگر هم به مرورگر میفرستم، که تگ head مربوط به فایل HTML است. در تگ head، تمامی تصاویر سنگین موجود در header و footer، و dns-prefetch باقی تصاویر را از پیش بارگذاری کردهام.
تگ head همچنین شامل لینک فایلهای CSS میباشد. حال همینطور که بخش اول بر روی سرور آماده میشود، مرورگر درخواست تمام منابع را در بخش head اعزام میکند، تا صفحه سریعتر پر شود.
با کمک افزونه Lighthouse در Chrome، کارایی هر دو صفحه آزمایش شد. نتایج واقعا دلگرم کننده هستند.
این آزمایش ۱۰ بار بر روی هر دو صفحه اجرا شد و تمام مقادیر در اینجا نمایش داده شدهاند:
این پیادهسازی پایه میتواند به شدت کارایی یک صفحه وب را ارتقا دهد. امیدوارم توانسته باشم این مفهوم را خوب توضیح دهم.
اگر شما هم میخواهید این برنامه را بر روی سیستم خود اجرا کنید، این مراحل را دنبال کنید:
۱. سورس کد را از اینجا clone کنید.
۲. Node را بر روی سیستم خود نصب کنید.
۳. npm install را بر روی سیستم خود اجرا کنید.
۴. npm run dev را اجرا کنید، تا فایل bundle.js را بسازید.
۵. پردازش را از بین ببرید و npm start را اجرا کنید.
۶. برنامه بر روی پورت ۸۰۸۰ اجرا خواهد شد.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید