تحسين أداء الموقع عن طريق شحن المزيد من CSS
هناك غريزة متأصلة في العمل الحديث لواجهات الواجهة الأمامية تقول إن أسرع صفحة هي التي تشحن أقل شيء: جافا سكريبت أقل، خطوط أقل، طلبات أقل. لذلك عندما كتب فريق هندسة GitHub عن تحسين أداء الموقع عن طريق شحن المزيد من CSS، بدا الأمر وكأنه تناقض. لم يكن الحل لواجهة بطيئة تعتمد على جافا سكريبت هو إزالة التنسيق من التطبيق. بل كان إعادة المزيد منه، كأوراق أنماط عادية وثابتة وقابلة للتخزين المؤقت، وحذف جافا سكريبت التي كانت موجودة فقط لتوليد الأنماط في المتصفح.
التمييز هو جوهر المقالة. شحن 'المزيد من CSS' لا يعني ورقة أنماط أكبر تثقل مسارك الحرج. بل يعني نقل توليد الأنماط من وقت التشغيل إلى وقت البناء، ثم ترك المتصفح يفعل ما هو بارع فيه حقًا: تحليل ورقة الأنماط مرة واحدة، وتخزينها مؤقتًا، وإعادة استخدامها عبر كل تنقل.
التكلفة الخفية للتنسيق في وقت التشغيل
حلت مكتبات CSS-in-JS مشاكل حقيقية. لقد أعطت مؤلفي المكونات نطاقًا (scoping) وتجميعًا (colocation) والقدرة على استيفاء الخصائص (props) في الأنماط دون اختراع اصطلاح تسمية. كان المقايضة معمارية: لقد نقلوا مشكلة وقت الترجمة إلى المتصفح.
توليد الأنماط هو عمل جافا سكريبت
كل صفحة يتم تقديمها من الخادم باستخدام مكتبة CSS-in-JS في وقت التشغيل تطلب من المتصفح القيام بنفس المهمة مرتين. يقوم الخادم بتسلسل الأنماط في علامات <style>، ثم تستيقظ مكتبة العميل، وتجول شجرة المكونات، وتقوم بتجزئة أسماء الفئات مرة أخرى، وتوفق النتيجة. يحدث هذا العمل على الخيط الرئيسي، متنافسًا مع خطوات التخطيط والطلاء التي تحاول تسريعها.
// Runtime CSS-in-JS: this stylesheet does not exist until hydration runs
const Button = styled.button`
background: ${(p) => (p.primary ? "#1f883d" : "transparent")};
border: 1px solid #d0d7de;
padding: 6px 16px;
border-radius: 6px;
font: inherit;
`;
نفس المكون المعبر عنه كورقة أنماط في وقت البناء يزيل كلاً من اعتماد وقت التشغيل وعمل التسلسل:
/* button.module.css, parsed once, cached forever */
.button {
padding: 6px 16px;
border: 1px solid #d0d7de;
border-radius: 6px;
font: inherit;
background: transparent;
}
.buttonPrimary {
background: #1f883d;
color: #ffffff;
}
import styles from "./button.module.css";
export function Button({ primary, ...props }) {
const className = primary
? `${styles.button} ${styles.buttonPrimary}`
: styles.button;
return <button className={className} {...props} />;
}
لا شيء جديد هنا. ما يسهل الاستهانة به هو مقدار الحزمة التي تمتلكها بهدوء مجموعة CSS-in-JS الناضجة: المكتبة نفسها، أدوات استيفاء الخصائص، مزود السمة، المسلسل، ورمز الإطار المكرر الذي ينمو عندما يعتمد كل حزمة على نسختها الخاصة.
تقع مباشرة على مسار العرض الحرج
علامة <link rel="stylesheet"> تمنع العرض، لكنها تمنعه مرة واحدة، ويمكن للمتصفح تحليل الملف بينما لا يزال تدفق HTML قادمًا. تعمل CSS في وقت التشغيل بشكل مختلف: لا يمكن للمتصفح حتى البدء في بناء أنماط المكونات حتى يتم تنزيل جافا سكريبت وتحليلها وتجميعها وتنفيذها. على هاتف متوسط المستوى، هذا ثانية أو أكثر من العمل على الخيط الرئيسي قبل أن يحصل زر واحد على لون خلفيته.
يجزئ ذاكرة التخزين المؤقت الخاصة بك
تتغير أسماء الفئات المولدة كلما تغير مصدر المكون، مما يعني أن ورقة الأنماط تبطل في كل نشر تقريبًا. قسم ذلك عبر عشرات الحزم على مستوى المسار وستحصل على معدل إصابة في ذاكرة التخزين المؤقت يبدو صحيًا في لوحة المعلومات لكنه نادرًا ما يعمل في الزيارات المتكررة. ورقة أنماط ثابتة ذات تجزئة للمحتوى، على النقيض، يمكن تقديمها مع Cache-Control: public, max-age=31536000, immutable ولا يتم تنزيلها أبدًا مرتين.
يضاعف العمل أثناء الترطيب
عدم التطابق في الترطيب بين الأنماط المحقونة من الخادم والمولدة من العميل هو مصدر كلاسيكي للوميض. يرسم المتصفح مجموعة واحدة من القواعد، ثم تستبدلها مكتبة العميل في منتصف التفاعل، مما يجبر على إعادة حساب النمط وأحيانًا تغيير في التخطيط. أسماء الفئات الثابتة المحسوبة في وقت البناء متطابقة على كلا الجانبين، لذا فإن الطلاء الأول هو أيضًا الطلاء النهائي.

ما غيرته GitHub بالفعل
من الأفضل فهم الترحيل على أنه خمس حركات معمارية بدلاً من إعادة هيكلة واحدة. لا تتطلب أي منها التخلي عن نموذج المكونات، ولا تتطلب أي منها إعادة كتابة كاملة.

أصبحت رموز التصميم خصائص CSS مخصصة
انتقلت مقاييس اللون والتباعد ونصف القطر والطباعة إلى كتلة :root عالمية كخصائص مخصصة. تشير المكونات إلى var(--color-bg-default) بدلاً من استيراد كائن سمة جافا سكريبت. هذا ما يجعل CSS الثابتة معبرة بما يكفي لاستبدال الأنماط الديناميكية في معظم الحالات، وتمكن السمة في وقت التشغيل (الوضع الداكن، التباين العالي) بدون جافا سكريبت.
تجميع أنماط المكونات مسبقًا
حصل كل مكون على ورقة أنماط حقيقية، تم إصدارها في وقت البناء ونطاقها بواسطة تجزئة أو اصطلاح وحدة. الناتج هو CSS عادي يمكن لأي متصفح تحليله بسرعة التحليل.
تضمين CSS الحرج في رأس المستند
فقط القواعد اللازمة لإطار العرض الأول يتم تضمينها في HTML. هذا عادةً 8-14 كيلوبايت مضغوطة، ويعني أن الطلاء الأول لا ينتظر أبدًا رحلة ذهاب وإياب عبر الشبكة لورقة الأنماط التي تشكل الصفحة المرئية.
<head>
<!-- Critical: exactly what the first viewport needs, nothing more -->
<style>
/* reset + tokens + header + hero + above-the-fold grid */
</style>
<!-- Everything else: one hashed, immutable, long-cached file -->
<link
rel="stylesheet"
href="/assets/app.8f3a1c2d.css"
media="print"
onload="this.media='all'"
/>
<noscript>
<link rel="stylesheet" href="/assets/app.8f3a1c2d.css" />
</noscript>
</head>
تحمل علامات التقديم من الخادم أسماء الفئات النهائية
نظرًا لأن أسماء الفئات موجودة في وقت البناء، فإن HTML الذي يعيده الخادم منسق بالفعل. لا توجد مرحلة ثانية، ولا خطوة توفيق، ولا وميض للمحتوى غير المنسق على الاتصالات البطيئة.
استبدلت طبقات التتالي اختراقات الخصوصية
نظام التصميم المشترك يعيش أو يموت على ترتيب التتالي القابل للتنبؤ. إعلان ترتيب الطبقات مرة واحدة، مقدمًا، يجعل التجاوزات حتمية ويزيل الحافز للوصول إلى !important أو محددات أعمق من أي وقت مضى.
@layer reset, tokens, base, components, utilities;
@layer components {
.card {
background: var(--color-canvas-default);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-2);
padding: var(--space-4);
}
}
@layer utilities {
.mt-4 { margin-top: var(--space-4); }
}
الأرقام: ما الذي يتغير عندما تشحن المزيد من CSS
نقل توليد الأنماط إلى وقت البناء يغير شكل ما تشحنه، وليس فقط الحجم. البايتات المتبقية رخيصة: فهي تضغط جيدًا، وتتم مشاركتها عبر كل مسار، ويتم تخزينها مؤقتًا لمدة عام.

يظهر العائد المعماري في أربعة أماكن في وقت واحد: جافا سكريبت أقل للتنزيل، عمل أقل على الخيط الرئيسي قبل الطلاء الأول، ورقة أنماط قابلة للتخزين المؤقت مشتركة عبر التنقلات، وترميز متطابق على الخادم والعميل.
| Approach | JS shipped (gzip) | CSS shipped (gzip) | Runtime style work | Cacheable across deploys |
|---|---|---|---|---|
| Runtime CSS-in-JS | ~180 KB | ~12 KB | High | No |
| Single monolithic stylesheet | ~96 KB | ~88 KB | None | Yes |
| Inline critical + split static sheets | ~78 KB | ~54 KB | None | Yes |
| Utility-first CSS | ~85 KB | ~40 KB | None | Yes |
أسرع ورقة أنماط هي تلك التي يحللها المتصفح مرة واحدة، ويخزنها مؤقتًا لمدة عام، ولا يضطر أبدًا إلى توليدها مرة أخرى.
دليل الترحيل العملي
لا تحتاج إلى قاعدة كود GitHub لتطبيق نفس المنطق. الترحيل تدريجي وكل خطوة قابلة للشحن بشكل مستقل.

1. جرد ما يتم عرضه فعليًا في وقت التشغيل
افتح DevTools، وحمل صفحة، وعد document.styleSheets. إذا وجدت عشرات من علامات <style> المحقونة بأسماء فئات مجزأة، فقد وجدت هدفك. قارن مع لوحة الأداء: ابحث عن مهام طويلة بين DOMContentLoaded وأول طلاء ذي محتوى.
2. استخراج CSS فوق الطية
ابدأ بأصغر كتلة مضمنة ممكنة. قاعدة تجريبية موثوقة هي تضمين إعادة التعيين والرموز والقواعد المستخدمة من قبل أول ارتفاعين لإطار العرض. كل شيء تحت الطية يتم تحميله من ورقة الأنماط المؤجلة.
3. تحويل مكونات وقت التشغيل إلى فئات ثابتة
اعمل مكونًا تلو الآخر، بدءًا من تلك ذات الحركة المرورية الأعلى. تصبح المتغيرات المدفوعة بالخصائص إما فئات معدلة أو خصائص مخصصة مضبوطة على غلاف. حافظ على واجهة برمجة التطبيقات للمكون متطابقة حتى لا تتغير مواقع الاستدعاء.
// Genuinely dynamic values still belong in an inline style, not a generated sheet
<div
className={styles.progressBar}
style={{ "--progress": `${percent}%` }}
role="progressbar"
aria-valuenow={percent}
/>
.progressBar {
width: var(--progress, 0%);
transition: width 200ms ease-out;
}
هذا النمط هو العمود الفقري للترحيل بأكمله: هيكل النمط ثابت وقابل للتخزين المؤقت، بينما القيمة تبقى ديناميكية ولا تكلف شيئًا للتحديث.
4. إنشاء ترتيب الطبقات مبكرًا
أعلن ترتيب @layer في أعلى ورقة أنماط الدخول الخاصة بك قبل أن يكون لديك مشكلة خصوصية، وليس بعدها. تعديل الطبقات على تتالي قديم هو أكثر إيلامًا بكثير من البدء بها.
5. تجزئة أسماء الملفات والتخزين المؤقت بشكل غير قابل للتغييرتجزئة المحتوى بالإضافة إلى immutable هو ما يحول "المزيد من CSS" إلى "CSS مجاني". يجب أن تصل عرض الصفحة الثانية في الجلسة إلى ذاكرة التخزين المؤقت HTTP لكل قاعدة نمط في الموقع.
6. حافظ على التنسيق الديناميكي الحقيقي ديناميكيًا
تنتمي القيم المستندة إلى التحويل وموضع التمرير والقياس إلى الأنماط المضمنة أو requestAnimationFrame أو واجهة برمجة تطبيقات الرسوم المتحركة للويب. لا تعيد تقديم محرك أنماط وقت التشغيل لحل مشكلة تحلها خصائص CSS المخصصة بالفعل.
كيفية قياس التحسن
قم بقياس نفس الأشياء الثلاثة قبل وبعد: توقيت الرسم، والمهام الطويلة في الخيط الرئيسي، وعدد أوراق الأنماط. يوفر لك نص PerformanceObserver صغير هذه الثلاثة دون إضافة تبعية مراقبة.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, Math.round(entry.startTime));
}
}).observe({ type: "paint", buffered: true });
new PerformanceObserver((list) => {
const long = list.getEntries().filter((e) => e.duration > 50);
console.log(`${long.length} long tasks after load`);
}).observe({ type: "longtask", buffered: true });
console.log("stylesheets:", document.styleSheets.length);
توقع أكبر تحسن في وقت أول رسم عند الزيارات المتكررة وعند ملفات تعريف الأجهزة المحمولة المقيدة، حيث تهيمن تكلفة تحليل JavaScript الملغاة. يميل أكبر رسم محتوى إلى التحسن بشكل تدريجي أكثر، إصدارًا بعد إصدار.

مخاطر تستحق التجنب
النهج ليس خاليًا من المخاطر، ومعظم الإخفاقات تأتي من تطبيقه بقوة مفرطة.

- **CSS حرج غير مكتمل.**إذا خمنت بشكل خاطئ حول منفذ العرض الأول، فستحصل على إعادة تدفق مرئية عند وصول الورقة المؤجلة. قم بإنشاء الكتلة الحرجة تلقائيًا من عرض حقيقي بدلاً من يدويًا. -**تضمين كل شيء.**تضمين ورقة الأنماط الكاملة في كل استجابة HTML يهزم التخزين المؤقت ويضخم حجم المستند. قم بتضمين الحد الأدنى، وأجل الباقي. -**غرائز تجميع كل شيء.**النصيحة القديمة بدمج كل CSS في ملف واحد ضخم تسبق HTTP/2. التقسيم حسب المسار غالبًا ما يكون أفضل الآن، طالما يتم تخزين الأساسيات المشتركة بشكل منفصل. -خلط نظامين. تشغيل CSS-in-JS في وقت التشغيل وأوراق الأنماط الثابتة جنبًا إلى جنب لفترة طويلة يضاعف سطح الأنماط ويجعل تصحيح أخطاء التتالي بائسًا. حدد موعدًا نهائيًا للهجرة لكل دليل مكون.
المخاطرة غير التقنية الأكثر شيوعًا هي معاملة هذا كإعادة كتابة شاملة. الفرق التي تنجح تقوم بتحويل عدد قليل من المكونات عالية الحركة، وتقيس الفرق، وتستخدم تلك البيانات لتبرير الدفعة التالية.
استخدام DivMagic لهندسة عكسية لمعمارية CSS حقيقية
جزء واحد غير مقدر من هذه الهجرة هو رؤية كيف تنظم المواقع الإنتاجية الناضجة أوراق الأنماط الخاصة بها. عندما تفحص موقعًا يقدم CSS ثابتًا بشكل جيد، يمكنك تعلم المزيد في خمس دقائق أكثر من ساعة من قراءة الوثائق: كيف يطبقون التتالي، كيف يحددون نطاق الرموز، كيف يقسمون الحرجة من المؤجلة، وكيف يسمون المعدلات.
تم بناء DivMagic خصيصًا لسير العمل هذا. بدلاً من نسخ أنماط العناصر يدويًا من DevTools قاعدة واحدة في كل مرة، يمكنك تحديد أي واجهة مستخدم على أي صفحة وسحب HTML وCSS الناتج مباشرة إلى محررك، جاهزًا للتكيف. هذا مفيد حقًا في حالتين: إنشاء نموذج أولي لمكون من تصميم مرجعي، ودراسة كيفية تنظيم موقع عالي الأداء تعجب به لتتالي أنماطه بحيث يمكنك عكس النمط في نظام التصميم الخاص بك.
كما أنه يقصر الجزء الممل من الخطوة الثالثة أعلاه. عندما تقوم بتحويل مكون وقت التشغيل إلى فئات ثابتة، فإن وجود الأنماط المحسوبة النهائية في شكل قابل للتحرير، بدلاً من أنماط مضمنة متفرقة وكتل <style> محقونة، يزيل معظم عمل النسخ.
النقاط الرئيسية
- توليد الأنماط في وقت التشغيل هو عمل JavaScript في الخيط الرئيسي يحدث بعد تنزيل JavaScript، مباشرة على مسار العرض الحرج الخاص بك.
- يتم تحليل CSS الثابت مرة واحدة، وتخزينه مؤقتًا لمدة عام عند تجزئة المحتوى، ومشاركته عبر كل مسار وكل تنقل.
- قم بتضمين فقط القواعد التي يحتاجها منفذ العرض الأول؛ وأجل الباقي خلف
<link>غير معطل وسياسة تخزين مؤقت طويلة. - خصائص CSS المخصصة بالإضافة إلى
@layerتغطي الغالبية العظمى من الحالات التي كانت تتطلب استيفاء الأنماط المستندة إلى الخصائص. - هاجر تدريجيًا، مكونًا تلو الآخر، وقم بقياس توقيت الرسم p75 والمهام الطويلة بدلاً من المتوسطات.
قد يبدو شحن المزيد من CSS وكأنه تراجع حتى تدرك أنك تستبدل ورقة أنماط مخزنة مؤقتًا وتحلل مرة واحدة بميغابايت من JavaScript التي تعيد بنائها في كل تحميل صفحة.
الدرس يعمم جيدًا خارج GitHub. كل تجريد يعمل في المتصفح هو تكلفة يدفعها المستخدم، على جهازه، على شبكته، في كل مرة. نقل توليد الأنماط إلى وقت البناء هو أحد أكثر المقايضات ذات النفوذ العالي المتاحة لواجهة أمامية تعتمد على المكونات، حيث تقوم بشحن ملف ثابت يعرف المتصفح بالفعل كيفية التعامل معه، وتحذف الآلية التي كانت تعترض طريقه.
