Containment و Performance در CSS: چطور `contain` و `content-visibility` رندرینگ رو متحول می‌کنن

یه چیزی که خیلی از دولوپرا نمی‌دونن اینه که یه خط CSS ساده مثل تغییر width می‌تونه کل صفحه رو دوباره محاسبه کنه، حتی اگه فقط یه المان کوچیک تو گوشه‌ی صفحه تغییر کرده باشه. این مقاله می‌خواد نشون بده مرورگر پشت پرده چیکار می‌کنه، چرا بعضی صفحات کند میشن، و چطور CSS مدرن (contain و content-visibility) بهت اجازه میده صریحاً به مرورگر بگی "فقط همین‌جا رو حساب کن، بقیه رو دست نزن".

۱. پایپ‌لاین رندرینگ مرورگر: از استایل تا پیکسل

قبل از هر چیز باید بدونیم وقتی یه صفحه رو لود می‌کنی یا استایلش تغییر می‌کنه، مرورگر چند مرحله رو طی می‌کنه:

Style  →  Layout (Reflow)  →  Paint  →  Composite
  • Style: مرورگر تعیین می‌کنه کدوم قوانین CSS به کدوم المان اعمال میشه.
  • Layout (Reflow): موقعیت و اندازه‌ی دقیق هر المان روی صفحه محاسبه میشه (اینجا سنگین‌ترین مرحله‌ست).
  • Paint: هر المان به پیکسل تبدیل میشه (رنگ، سایه، border و...).
  • Composite: لایه‌ها روی هم قرار می‌گیرن و نتیجه‌ی نهایی روی صفحه نمایش داده میشه.

نکته‌ی مهم اینه که Layout معمولاً global عمل می‌کنه. یعنی اگه اندازه‌ی یه المان تغییر کنه، مرورگر مجبوره چک کنه این تغییر رو المان‌های بعدی و حتی گاهی قبلی هم تاثیر گذاشته یا نه. برای همین یه تغییر کوچیک تو یه صفحه‌ی بزرگ با هزاران المان، می‌تونه کل درخت رو دوباره محاسبه کنه.

۲. Layout Thrashing: وقتی جاوااسکریپت و CSS با هم دشمن میشن

یه باگ کلاسیک performance اینه که تو یه حلقه، پشت سر هم بخونی و بنویسی به layout:

// خطرناک: هر تکرار یه Layout جدید اجباری می‌کنه
elements.forEach((el) => {
  const height = el.offsetHeight; // خوندن (Layout رو force می‌کنه)
  el.style.height = height + 10 + 'px'; // نوشتن (Layout رو dirty می‌کنه)
});

اینجا هر بار که offsetHeight رو می‌خونی، مرورگر مجبوره Layout رو فوراً (synchronously) دوباره محاسبه کنه چون مقدار قبلی dirty شده. به این میگن Layout Thrashing. راه‌حل معمول اینه که همه‌ی خوندن‌ها رو اول انجام بدی، بعد همه‌ی نوشتن‌ها:

// درست: اول همه‌ی خوندن‌ها، بعد همه‌ی نوشتن‌ها
const heights = elements.map((el) => el.offsetHeight);
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px';
});

ولی این فقط سمت جاوااسکریپته. سوال اصلی اینه: چطور به خود CSS بگیم "این بخش از صفحه مستقل از بقیه‌ست، لازم نیست کل درخت رو چک کنی"؟ اینجا contain وارد میشه.

۳. Property contain: مرز کشیدن دور تاثیرات یه المان

contain به مرورگر یه قول رسمی میده: "تغییرات داخل این المان روی بیرونش تاثیر نمی‌ذاره". با این قول، مرورگر می‌تونه محاسبات رو محدود کنه به همون زیردرخت، به‌جای کل صفحه.

.card {
  contain: layout;
}

چهار تا مقدار اصلی داره که هر کدوم یه نوع ایزوله‌سازی می‌کنن:

مقدارچی رو ایزوله می‌کنه
layoutمحاسبات layout داخل این المان از بیرون تاثیر نمی‌گیره و نمی‌ذاره (المان یه "جعبه‌ی مستقل" میشه)
paintهیچی از داخل این المان بیرون از مرزهاش کشیده نمیشه (مثل یه overflow: hidden ضمنی، ولی قوی‌تر)
sizeاندازه‌ی این المان مستقل از محتواشه؛ باید خودت اندازه بدی وگرنه ممکنه صفر بشه
stylecounter و quote‌های CSS از این المان بیرون درز نمی‌کنه

معمولاً این‌ها رو با هم ترکیب می‌کنن، یا از مقدار میان‌بر strict / content استفاده می‌کنن:

/* معادل: contain: size layout paint style */
.widget {
  contain: strict;
}

/* معادل: contain: layout paint style (بدون size) */
.card {
  contain: content;
}

چرا size بدون تعیین اندازه خطرناکه؟

اگه contain: size بدی ولی خودت width/height مشخص نکنی، مرورگر فرض می‌کنه المان هیچ محتوایی نداره و ممکنه اندازه‌ش صفر بشه. برای همین همیشه باید یه اندازه‌ی ثابت یا حداقلی بدی:

.widget {
  contain: strict;
  width: 300px;
  height: 200px; /* اجباریه وگرنه محتوا جمع میشه */
}

۴. content-visibility: قهرمان واقعی صفحات طولانی

contain خوبه ولی محاسبات رو محدود می‌کنه، مانعشون نمیشه. اینجا content-visibility وارد میشه؛ این property به مرورگر اجازه میده کلاً از محاسبه‌ی Layout و Paint یه المان که بیرون از viewport‌ه، صرف‌نظر کنه.

.section {
  content-visibility: auto;
}

با content-visibility: auto، مرورگر برای المان‌هایی که دور از دید کاربرن، Layout و Paint رو کلاً skip می‌کنه، انگار که وجود ندارن، تا وقتی نزدیک viewport بشن. این برای صفحات با لیست‌های خیلی طولانی (فید، جدول‌های بزرگ، آرشیو مقالات) تفاوت فاحشی تو زمان رندر اولیه ایجاد می‌کنه، گاهی چند برابر سریع‌تر.

مشکل: اندازه‌ی اسکرول‌بار بهم می‌ریزه

وقتی مرورگر محاسبه‌ی یه بخش رو skip می‌کنه، فرض می‌کنه ارتفاعش صفره، مگر اینکه بهش بگی چقدر تقریباً باید باشه. این باعث میشه اسکرول‌بار صفحه هی بپره. راه‌حلش contain-intrinsic-sizeه:

.section {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px; /* تخمین ارتفاع قبل از رندر واقعی */
}

این یعنی: "تا وقتی محتوای واقعی رندر نشده، فرض کن ۵۰۰ پیکسل ارتفاع داره" تا جای خالیش تو صفحه drزerved بمونه و اسکرول‌بار درست عمل کنه.

مثال کامل: لیست طولانی با performance بهتر

<div class="feed">
  <article class="feed-item">...</article>
  <article class="feed-item">...</article>
  <!-- صدها آیتم دیگه -->
</div>
.feed-item {
  content-visibility: auto;
  contain-intrinsic-size: auto 400px;
}

با این دو خط، مرورگر فقط آیتم‌های نزدیک به viewport رو واقعاً محاسبه می‌کنه و بقیه رو "منجمد" نگه می‌داره، بدون اینکه لازم باشه از virtualization سنگین جاوااسکریپتی (مثل react-window) استفاده کنی.

۵. تفاوت با display: none و visibility: hidden

شاید بپرسی مگه display: none همین کار رو نمی‌کنه؟ نه، فرقشون اساسیه:

Propertyرفتار
display: noneالمان کلاً از accessibility tree و رندر حذف میشه؛ برای نمایش دوباره باید کامل از صفر رندر بشه
content-visibility: hiddenمثل display: none عمل می‌کنه ولی state رندر داخلی رو cache نگه می‌داره؛ برگردوندنش خیلی سریع‌تره
content-visibility: autoخودکار تصمیم می‌گیره؛ اگه تو viewport نیست skip می‌کنه، اگه هست رندر می‌کنه

یعنی اگه یه tab-panel داری که مدام باز و بسته میشه، content-visibility: hidden خیلی بهتر از display: none عمل می‌کنه چون مرورگر مجبور نیست هر بار از صفر Layout/Paint رو حساب کنه.

۶. چرا این‌ها فقط "بهینه‌سازی جزئی" نیستن

نکته‌ی مهمی که این مبحث رو عمیق می‌کنه اینه که contain و content-visibility فقط hint ساده نیستن، بلکه مستقیم رو الگوریتم رندرینگ مرورگر اثر می‌ذارن. یه المان با contain: layout باعث میشه مرورگر بتونه Layout رو موازی یا حداقل مستقل محاسبه کنه، چون می‌دونه این زیردرخت به بیرون وابسته نیست. این دقیقاً همون چیزیه که باعث میشه صفحات بزرگ (داشبوردها، لیست‌های بی‌نهایت، ادیتورهای پیچیده) با یه تغییر کوچیک CSS، محسوس سریع‌تر بشن؛ بدون اینکه حتی یه خط جاوااسکریپت اضافه کنی.

جمع‌بندی

Layout مرورگر به‌صورت پیش‌فرض global فکر می‌کنه: هر تغییری ممکنه همه‌جا اثر بذاره. contain بهت اجازه میده صریحاً مرز بکشی و به مرورگر بگی "این بخش مستقله". content-visibility یه قدم جلوتر میره و کلاً محاسبه‌ی بخش‌های دور از دید رو حذف می‌کنه. ترکیب این دو با contain-intrinsic-size، یکی از قوی‌ترین ابزارهای performance تو CSS مدرنه؛ ابزاری که خیلی از دولوپرا حتی اسمشو نشنیدن، ولی می‌تونه performance صفحات سنگین رو چند برابر کنه.