وقتی CSS مدرن فرانتاند رو پیچیدهتر میکنه
خلاصهٔ کاملتر
Gabriel Shoyombo تو این مقاله یه نکته مهم رو یادآوری میکنه: وقتی پنجاه خط جاوااسکریپت رو با چند خط CSS عوض میکنی، diff قشنگ به نظر میاد، ولی یه هفته بعد یه کارت از داشبورد غیب میشه یا فاصلهها به هم میریزه. نویسنده بین سادگی محلی (یعنی یه فایل چقدر کد کمی لازم داره) و پیچیدگی سیستم (یعنی چند تا تعامل رو باید بفهمی تا بتونی با خیال راحت تغییر بدی) فرق میذاره. به گفتهاش، یه فیچر جدید فقط وقتی پیچیدگی رو کم میکنه که واقعاً جای قبلی رو بگیره و بقیه چیزها رو به هم نریزه.
سناریوی اول «رفتار دو صاحبه»ست. فرض کن کارتی با ResizeObserver (یعنی API جاوااسکریپتی که تغییر اندازه المنت رو گزارش میده) کلاس card--wide میگرفت و با container query عوضش کردی. ولی observer تو یه فایل دیگه زنده مونده و یه rule قدیمی هنوز اون کلاس رو میخونه. وقتی breakpoint از 480px به 600px برده میشه، بین این دو عدد کارت یه حالت قاطی پیدا میکنه که هیچکس ننوشته. توصیه نویسنده: CSS صاحب ظاهری باشه که از اندازه یا ساختار DOM میاد، JS صاحب state اپ و کارهای async، و بعد از مهاجرت حتماً کد قدیمی رو کامل پاک کن.
سناریوی دوم اینه که container-type: inline-size خیلی بیشتر از «بچههام عرضم رو query کنن» کار میکنه. این دستور یه block formatting context (یعنی یه محدوده layout مستقل) میسازه، پس margin collapse متوقف میشه، کارت کنار float جابهجا میشه و از همه بدتر، کارتی که عرضش از محتواش میاد (مثلاً flex item یا fit-content) به 0px میرسه. تو تست نویسنده سه کارت با عرض حدود 74px، 96px و 114px همه صفر شدن. راهحل: یا از context بهش عرض بده یا container-type رو روی یه wrapper بذار.
سناریوی سوم درباره cascade و specificity هست. سلکتور زیر باید فقط کارتهایی با عکس hero رو هدف بگیره، ولی عکس آواتار کامنت هم حساب میشه و پاراگرافهای کامنت هم marginشون رو از دست میدن، چون specificity این سلکتور 0-1-2 هست و از .comment p با 0-1-1 میبره:
.card:has(img) p {
margin: 0;
}راهحل اینه که با child combinator محدودش کنی (.card:has(> img) > p). nesting هم همین مشکل رو داره، چون & بیشترین specificity لیست سلکتور رو قرض میگیره؛ با :where() که همیشه 0-0-0 هست میشه وزنش رو صفر کرد. نویسنده یادآوری میکنه که ruleهای داخل @layer همیشه به ruleهای بدون layer میبازن، پس یه کدبیس نصفهلایهای فقط یه ترتیب دیگه به ذهنت اضافه میکنه.
سناریوی چهارم عملکرده. نویسنده تو Chrome 153 یه لیست ۵۰۰۰ ردیفی تست کرده. یه :has() روی body که MDN ازش منع میکنه فقط حدود 1.4ms خرج داشت و یه المنت رو restyle کرد. ولی دو rule بیخطر با :has() کنار هم باعث شدن هر toggle همه ۵۰۰۱ المنت رو restyle کنه (حدود 7ms) و با ۲۰ هزار ردیف به حدود 31ms رسید که تقریباً دو فریمه. این رفتار رو به تیم Chromium گزارش داده. پیامش اینه که «بردیمش تو CSS پس سریعتره» رو فرض نکن و با DevTools اندازه بگیر.
نکات کلیدی:
- container-type: inline-size یه block formatting context میسازه و میتونه کارتهای content-sized رو به عرض 0px برسونه.
- specificity سلکتور .card:has(img) p برابر 0-1-2 هست و از .comment p (0-1-1) میبره؛ با > و :where() کنترلش کن.
- ruleهای داخل cascade layer به ruleهای بدون layer میبازن، مگه اینکه !important باشه.
- دو rule با :has() کنار هم تو لیست ۵۰۰۰ ردیفی حدود 7ms و ۵۰۰۱ restyle در هر toggle هزینه داشتن.
- چکلیست مهاجرت شش سؤال داره: مالکیت، scope، cascade، مهاجرت کامل، عملکرد و نگهداریپذیری.




