Render Blocking و Critical CSS؛ اولین چیزی که کاربر می‌بینه رو سریع‌تر برسونید

تو درس قبل به این نکته اشاره کردیم که CSS به‌طور پیش‌فرض «مسدودکننده‌ی رندر» هست؛ یعنی مرورگر تا وقتی فایل CSS رو کامل نگیره و پردازش نکنه، هیچی رو نشون نمی‌ده. حالا می‌خوایم دقیق‌تر ببینیم این مکانیزم واقعاً چطور کار می‌کنه، و چطور می‌شه با تکنیک‌هایی مثل Critical CSS، این تأخیر رو تا حد زیادی کاهش داد.

چرا اصلاً CSS باید رندر رو مسدود کنه؟

این رفتار تصادفی یا یه محدودیت فنی ناخواسته نیست؛ عمداً همین‌جوری طراحی شده. اگه مرورگر HTML رو بدون منتظر موندن برای CSS رندر کنه، کاربر اول یه صفحه‌ی کاملاً بدون‌استایل می‌بینه (متن‌های بزرگ سیاه، بدون چیدمان)، و چند لحظه بعد ناگهان همه‌چیز به شکل نهایی می‌پره. این پدیده «فلش محتوای بدون‌استایل» یا FOUC نام داره، و تجربه‌ی بصری خیلی بدتری نسبت به یه تأخیر کوتاه با صفحه‌ی خالیه. پس مرورگرها ترجیح می‌دن صبر کنن و یه‌جا کامل رندر کنن.

مسیر بارگذاری یک صفحه: چرا ترتیب لینک‌ها مهمه

وقتی مرورگر HTML رو پردازش می‌کنه، به محض رسیدن به یه تگ <link rel="stylesheet">، دانلود اون فایل رو شروع می‌کنه و رندر رو متوقف نگه می‌داره تا اون فایل کامل بیاد و پردازش بشه:

<head>
  <link rel="stylesheet" href="styles.css">
</head>

اگه این فایل بزرگ باشه، یا سرور کند جواب بده، کاربر یه صفحه‌ی کاملاً سفید رو برای مدت قابل‌توجهی می‌بینه. هرچی این فایل کوچیک‌تر و سریع‌تر برسه، زودتر چیزی رو صفحه ظاهر می‌شه.

Critical CSS چیه؟

ایده‌ی اصلی Critical CSS اینه: به‌جای این‌که منتظر بمونیم کل CSS سایت (که ممکنه شامل استایل‌های صفحاتی باشه که کاربر اصلاً الان نمی‌بینه) دانلود بشه، فقط اون بخشی از CSS که برای رندر «بالای صفحه» (above the fold؛ یعنی چیزی که بدون اسکرول دیده می‌شه) لازمه رو مستقیم و فوری تو خود HTML قرار بدیم:

<head>
  <style>
    /* فقط استایل‌های حیاتی برای بالای صفحه */
    body { margin: 0; font-family: sans-serif; }
    .header { background: #1a1a1a; color: white; padding: 16px; }
    .hero { min-height: 60vh; display: flex; align-items: center; }
  </style>
  <link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
</head>

این‌جوری، مرورگر بلافاصله (بدون نیاز به منتظر موندن برای یه فایل خارجی) می‌تونه بالای صفحه رو با استایل درست رندر کنه، و بقیه‌ی CSS (که تو تگ link جداست) در پس‌زمینه لود می‌شه.

ترفند media="print" برای لود غیرمسدودکننده

تو مثال بالا، یه ترفند جالب دیدید: media="print" باعث می‌شه مرورگر این استایل‌شیت رو «مسدودکننده‌ی رندر» در نظر نگیره (چون فرض می‌کنه فقط برای چاپه، نه نمایش فوری)، ولی همچنان دانلودش می‌کنه. بعد وقتی لود تموم شد، رویداد onload مقدار media رو به all تغییر می‌ده تا استایل‌ها روی صفحه‌ی نمایش هم اعمال بشن. این یه راه استاندارد و شناخته‌شده برای لود «async» کردن CSSه، بدون نیاز به جاوااسکریپت پیچیده.

چطور Critical CSS رو استخراج کنیم؟

نوشتن دستی Critical CSS برای یه سایت بزرگ عملاً غیرممکنه؛ معمولاً از ابزارهای اتوماتیک (که بخشی از فرآیند build پروژه هستن) استفاده می‌شه که یه صفحه رو باز می‌کنن، می‌بینن دقیقاً چه CSS ای برای رندر ناحیه‌ی قابل‌مشاهده‌ی اولیه لازمه، و اون رو جدا استخراج می‌کنن. این ابزارها معمولاً بخشی از فرآیند build (نه چیزی که خودتون دستی بنویسید) هستن، ولی دونستن این‌که این مفهوم چیه و چرا مهمه، به شما کمک می‌کنه بفهمید چرا بعضی پروژه‌ها این‌جوری ساختاربندی شدن.

preload برای فونت‌ها و منابع حیاتی دیگه

یه تکنیک مرتبط دیگه، استفاده از rel="preload" برای منابعی هست که CSS بهشون وابسته‌ست، مثل فونت‌ها:

<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

بدون preload، مرورگر معمولاً اول باید CSS رو کامل پردازش کنه تا بفهمه به یه فونت خاص نیاز داره، بعد شروع به دانلودش کنه؛ preload این ترتیب رو می‌شکنه و دانلود فونت رو زودتر (موازی با خود CSS) شروع می‌کنه.

@import: چرا معمولاً توصیه نمی‌شه؟

یه اشتباه رایج، استفاده از @import داخل CSS برای تقسیم فایل‌هاست:

/* داخل styles.css */
@import url("base.css");
@import url("components.css");

مشکل اینجاست: مرورگر نمی‌تونه این import ها رو موازی دانلود کنه؛ باید اول styles.css رو بگیره، پردازشش کنه، بعد تازه بفهمه باید base.css رو هم بگیره، و این کار رو به‌ترتیب و پشت سر هم انجام می‌ده (زنجیره‌ای، نه موازی). این باعث می‌شه زمان رسیدن به رندر نهایی خیلی بیشتر از حالتی بشه که همون فایل‌ها مستقیم با چند تگ link جدا تو HTML لینک شده باشن (که مرورگر می‌تونه همه‌شون رو هم‌زمان دانلود کنه).

نکته مهم: اگه از یه bundler (مثل Vite یا Webpack) استفاده می‌کنید، معمولاً @import رو تو مرحله‌ی build با هم ادغام می‌کنه و این مشکل از بین می‌ره؛ مشکل اصلی وقتیه که @import مستقیم تو مرورگر و بدون build process اجرا بشه.

اشتباه رایج: تقسیم بیش‌ازحد فایل‌های CSS

بعضی توسعه‌دهنده‌ها فکر می‌کنن تقسیم CSS به خیلی فایل کوچیک (یکی برای هر کامپوننت) خودکار پرفورمنس رو بهتر می‌کنه. ولی هر فایل CSS جدا، یعنی یه درخواست شبکه‌ی جداگانه، و هر درخواست هزینه‌ی خودش (مثل زمان اتصال) رو داره. تعادل درست معمولاً اینه: فایل‌های حیاتی و کوچیک رو inline کنید، و بقیه رو تو یه یا چند فایل معقول (نه ده‌ها فایل ریز) به‌صورت غیرمسدودکننده لود کنید.

جمع‌بندی

CSS به‌طور پیش‌فرض رندر صفحه رو مسدود می‌کنه تا از FOUC جلوگیری کنه؛ این یعنی حجم و سرعت رسیدن فایل CSS مستقیم روی زمانی که کاربر اولین‌بار چیزی می‌بینه اثر داره. تکنیک Critical CSS (استایل‌های حیاتی inline داخل head، بقیه به‌صورت async با ترفند media="print") این تأخیر رو به‌شدت کاهش می‌ده. از @import تو مرورگر (بدون build process) پرهیز کنید چون زنجیره‌ای لود می‌شه، و preload رو برای منابع حیاتی مثل فونت در نظر داشته باشید.