{"id":"l-css-4-5","courseId":"css-mastery","lang":"ar","kind":"lesson","summary":"اعرف أيّ تغييرات CSS تستدعي التخطيط أو الرسم أو التركيب وحده، وحرّك الخصائص الرخيصة، واستخدم content-visibility وaspect-ratio لتبقى الصفحات الطويلة سريعة ومستقرّة.","html":"<p>بدا جدول Waypoint مكتملًا، ثم فتحه الفريق على هاتف Android عمره ثلاث سنوات عبر شبكة Wi-Fi المزدحمة في قاعة المؤتمر. تقطّع إشعار الحفظ، وبدا التمرير الأول لزجًا، ودفعت صور المتحدّثين النص إلى الأسفل وهي تُحمَّل. لم يكن شيء معطوبًا؛ كانت الصفحة ببساطة تؤدّي عملًا أكثر مما تحتاج.</p>\n<p>أداء CSS يعود إلى سؤال واحد: <em>كم من مراحل خط الرسم يجبر هذا التغيير المتصفّح على تشغيلها؟</em></p>\n<h2 id=\"sec-1\" tabindex=\"-1\">خط الرسم في المتصفّح</h2>\n<p>بعد أن تغيّر الـ CSS شيئًا ما، يشغّل المتصفّح حتى أربع مراحل لتحديث الشاشة:</p>\n<ol>\n<li><strong>الأنماط (Style)</strong>: تحديد القواعد المنطبقة وحساب قيم كل عنصر.</li>\n<li><strong>التخطيط (Layout)</strong>: حساب حجم كل صندوق وموضعه.</li>\n<li><strong>الرسم (Paint)</strong>: ملء البكسلات: النصوص والألوان والحدود والظلال والصور.</li>\n<li><strong>التركيب (Composite)</strong>: تجميع الطبقات المرسومة على الـ GPU، مع تطبيق الـ transforms والشفافية.</li>\n</ol>\n<p>السرّ أن كل تغيير لا يحتاج إلا إلى البدء من مرحلة معيّنة. غيّر <code dir=\"ltr\">width</code> فيجب أن يعمل التخطيط، ثم الرسم، ثم التركيب. وغيّر <code dir=\"ltr\">background-color</code> فيمكن تخطّي التخطيط. وغيّر <code dir=\"ltr\">transform</code> أو <code dir=\"ltr\">opacity</code> على عنصر له طبقة خاصة به، فيستطيع المتصفّح تخطّي التخطيط <em>والرسم</em> معًا، وتحريك بكسلات موجودة أصلًا على الـ GPU.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-css-4-5-t1\"><defs><marker id=\"arrow-l-css-4-5-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-css-4-5-t1\">أربع مراحل لخط الرسم من اليسار إلى اليمين: الأنماط، التخطيط، الرسم، التركيب. صف لـ width وtop وmargin يمرّ بالأربع كلها. وصف لـ color وbackground وbox-shadow يتخطّى التخطيط. وصف لـ transform وopacity يتخطّى التخطيط والرسم.</title>\n  <rect class=\"d-box\" x=\"170\" y=\"16\" width=\"110\" height=\"36\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"196\" y=\"40\">الأنماط</text>\n  <rect class=\"d-box\" x=\"295\" y=\"16\" width=\"110\" height=\"36\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"319\" y=\"40\">التخطيط</text>\n  <rect class=\"d-box\" x=\"420\" y=\"16\" width=\"110\" height=\"36\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"451\" y=\"40\">الرسم</text>\n  <rect class=\"d-box\" x=\"545\" y=\"16\" width=\"120\" height=\"36\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"557\" y=\"40\">التركيب</text>\n  <text class=\"d-code\" x=\"10\" y=\"94\">width, top</text>\n  <rect class=\"d-box-warn\" x=\"170\" y=\"76\" width=\"495\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"180\" y=\"95\">المراحل الأربع، في كل إطار</text>\n  <text class=\"d-code\" x=\"10\" y=\"144\">box-shadow</text>\n  <rect class=\"d-box-accent\" x=\"170\" y=\"126\" width=\"110\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-accent\" x=\"420\" y=\"126\" width=\"245\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"303\" y=\"145\">مُتخطّاة</text>\n  <text class=\"d-code\" x=\"10\" y=\"194\">translate</text>\n  <rect class=\"d-box-success\" x=\"170\" y=\"176\" width=\"110\" height=\"28\" rx=\"6\"/>\n  <rect class=\"d-box-success\" x=\"545\" y=\"176\" width=\"120\" height=\"28\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"370\" y=\"195\">مُتخطّاة</text>\n</svg><figcaption>أيّ المراحل يستدعيها تغيير الخاصية</figcaption></figure>\n<p>في نقرة واحدة لا يهمّ الفرق. أما في حركة تعمل 60 مرة في الثانية، أو تغيير نمط يلمس 500 عنصر، فهو ما يقرّر هل ستبدو الصفحة سلسة أم لا.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">حرّك الخصائص الرخيصة</h2>\n<p>كان إشعار الحفظ يتحرّك هكذا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@keyframes</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> toast-in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">left</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">-320</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  to</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">left</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">left</code> خاصية أبعاد: كل إطار يعيد تشغيل التخطيط للإشعار (ويفحص هل تحرّك أيّ شيء آخر)، ثم يعيد الرسم. والإصلاح يُبقي موضع الإشعار ثابتًا ويحرّك صورته بدلًا من ذلك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.toast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  position</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">fixed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  left</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  bottom</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: toast-in </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">250</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@keyframes</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> toast-in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">-336</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  to</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;      </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>النتيجة البصرية نفسها، بجزء صغير من العمل. ولهذا استخدمت كل حركة في هذه الدورة <code dir=\"ltr\">translate</code> و<code dir=\"ltr\">scale</code> و<code dir=\"ltr\">opacity</code> وأخواتها. والمنطق نفسه ينطبق على الـ transitions: مرورٌ يغيّر <code dir=\"ltr\">box-shadow</code> يعيد الرسم في كل إطار، وهذا مقبول لبطاقة واحدة وملحوظ لشبكة من خمسين بطاقة تتحرّك معًا. وإظهار ظلّ مرسوم مسبقًا على pseudo-element بتحريك <code dir=\"ltr\">opacity</code> هو البديل الأرخص حين يهمّ الأمر.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>will-change في كل مكان</span></p>\n<p><code dir=\"ltr\">will-change: transform</code> تطلب من المتصفّح أن يرفع العنصر إلى طبقة خاصة به مسبقًا. على العنصر الوحيد الذي يوشك أن يتحرّك، قد يتجنّب ذلك تعثّرًا في البداية. أما على كل بطاقة «احتياطًا»، فتنشئ عشرات الطبقات التي تكلّف كلٌّ منها ذاكرة من الـ GPU، وقد يجعل ذلك الصفحة أبطأ، خصوصًا على الهواتف. أضفها قبيل الحركة مباشرةً (أو عند <code dir=\"ltr\">:hover</code> على العنصر الأب) ثم تخلَّ عنها بعدها؛ وفي الغالب لا تحتاج إليها أصلًا.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">تخطَّ العمل الذي لا تراه</h2>\n<p>يرسم الجدول جلسات يومين، لكن المستخدم يرى ملء شاشة واحدة في كل مرة. <code dir=\"ltr\">content-visibility: auto</code> تتيح للمتصفّح تخطّي التخطيط والرسم للأقسام الواقعة خارج الشاشة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.day</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  content-visibility</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">auto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  contain-intrinsic-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">auto</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>المحتوى المتخطّى ليس له حجم، لذا تعطي <code dir=\"ltr\">contain-intrinsic-size</code> المتصفّح تقديرًا يستخدمه كعنصر نائب؛ ومع <code dir=\"ltr\">auto</code> يتذكّر الحجم الحقيقي بعد أن يُرسم القسم مرة. ودونها تنهار الأقسام إلى الصفر ويقفز شريط التمرير أثناء التمرير. يبقى المحتوى في الـ DOM وفي شجرة إمكانية الوصول، ويظل البحث داخل الصفحة يعمل. و<code dir=\"ltr\">content-visibility: auto</code> ضمن Baseline منذ سبتمبر 2025، حين أضاف Safari 26 القيمة <code dir=\"ltr\">auto</code>؛ والمتصفّح الذي لا يدعمها يرسم كل الأقسام ببساطة، كما كان يفعل من قبل.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">لا تحرّك الأشياء بعد ظهورها</h2>\n<p>مقياس Cumulative Layout Shift، أحد مقاييس Core Web Vitals، يقيس قفز المحتوى أثناء تحميل الصفحة. وفي CSS يكون المذنب المعتاد وسائط بلا مساحة محجوزة. صورة متحدّث لها <code dir=\"ltr\">width: 100%</code> بلا ارتفاع يكون ارتفاعها 0px حتى تصل الصورة، ثم تدفع كل شيء إلى الأسفل. احجز المساحة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.speaker</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> img</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  width</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">100</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  height</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">auto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  aspect-ratio</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  object-fit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">cover</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>سمتا <code dir=\"ltr\">width</code> و<code dir=\"ltr\">height</code> على <code dir=\"ltr\">&lt;img&gt;</code> تؤدّيان العمل نفسه، لأن المتصفّحات تستنتج منهما نسبة الأبعاد. وللخطوط من الويب، <code dir=\"ltr\">font-display: swap</code> مع خط احتياطي بمقاييس مشابهة تمنع النص من إعادة الانسياب حين يُحمَّل الخط.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>قِس أولًا</span></p>\n<p>افتح لوحة Performance في DevTools، وسجّل تمريرًا أو تفاعلًا، وابحث عن الكتل البنفسجية الطويلة (الأنماط والتخطيط) والخضراء (الرسم). وخيار «Paint flashing» في لوحة Rendering يُبرز ما يُعاد رسمه. حسّن ما يُظهره التسجيل، لا ما تشكّ فيه.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">أين أنت الآن</h2>\n<p>على مدى عشرين درسًا نقلت جدول Waypoint من ملف أنماط يصارعك إلى ملف تستطيع توقّعه: الـ layers و<code dir=\"ltr\">:where()</code> تحسم التعارضات، والـ custom properties تكشف واجهات الـ components، وgrid والـ subgrid والـ container queries تتولّى التخطيط، و<code dir=\"ltr\">oklch()</code> و<code dir=\"ltr\">light-dark()</code> تتولّيان الألوان، والخصائص المنطقية تجعله يعمل بالعربية، والحركة لطيفة ورخيصة. والعادة التي تستحق أن تحتفظ بها هي عادة الدرس الأول: قبل أن تغيّر الـ CSS، توقّع ما سيفعله المتصفّح، ثم تحقّق.</p>\n","toc":[{"id":"sec-1","text":"خط الرسم في المتصفّح","level":2},{"id":"sec-2","text":"حرّك الخصائص الرخيصة","level":2},{"id":"sec-3","text":"تخطَّ العمل الذي لا تراه","level":2},{"id":"sec-4","text":"لا تحرّك الأشياء بعد ظهورها","level":2},{"id":"sec-5","text":"أين أنت الآن","level":2}],"takeaways":["كل تغيير بصري يمرّ ببعض مراحل الأنماط والتخطيط والرسم والتركيب؛ خصائص الأبعاد تستدعي التخطيط، والخصائص البصرية تستدعي الرسم، و<code dir=\"ltr\">transform</code> و<code dir=\"ltr\">opacity</code> تستطيعان تخطّي الاثنين.","حرّك <code dir=\"ltr\">translate</code> و<code dir=\"ltr\">scale</code> و<code dir=\"ltr\">rotate</code> و<code dir=\"ltr\">opacity</code>؛ أما تحريك <code dir=\"ltr\">left</code> أو <code dir=\"ltr\">width</code> أو <code dir=\"ltr\">margin</code> فيعيد تشغيل التخطيط في كل إطار.","<code dir=\"ltr\">content-visibility: auto</code> مع <code dir=\"ltr\">contain-intrinsic-size</code> تتيح للمتصفّح تخطّي رسم الأقسام الواقعة خارج الشاشة في الصفحة الطويلة.","حجز المساحة بـ <code dir=\"ltr\">aspect-ratio</code> (أو بسمتي width وheight) يمنع إزاحات التخطيط حين تُحمَّل الصور والمحتوى المضمّن.","قِس قبل أن تحسّن، باستخدام لوحة Performance في DevTools وخيار وميض الرسم (paint flashing)، وأزل <code dir=\"ltr\">will-change</code> حين تنتهي الحركة."],"further":[{"title":"Rendering performance (web.dev)","url":"https://web.dev/articles/rendering-performance"},{"title":"content-visibility (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/content-visibility"},{"title":"Optimize Cumulative Layout Shift (web.dev)","url":"https://web.dev/articles/optimize-cls"}],"quiz":[{"q":"<p>إشعار منبثق ينزلق إلى الداخل بتحريك <code dir=\"ltr\">left</code> من <code dir=\"ltr\">-320px</code> إلى <code dir=\"ltr\">16px</code>. على هاتف متوسّط المواصفات يتقطّع. ما أفضل إصلاح؟</p>","options":[{"text":"أضف <code dir=\"ltr\">will-change: left</code> إلى الإشعار.","why":"<code dir=\"ltr\">will-change</code> لا تستطيع جعل <code dir=\"ltr\">left</code> رخيصة؛ تغييرها يظل يعيد تشغيل التخطيط في كل إطار."},{"text":"حرّك <code dir=\"ltr\">translate</code> من <code dir=\"ltr\">-336px 0</code> إلى <code dir=\"ltr\">0 0</code> بدلًا من ذلك، مع إبقاء <code dir=\"ltr\">left</code> ثابتة.","why":"صحيح. <code dir=\"ltr\">translate</code> لا تغيّر إلا طريقة تركيب الإشعار المرسوم أصلًا، فيستطيع المتصفّح تخطّي التخطيط والرسم أثناء الحركة."},{"text":"قصّر الحركة إلى 50ms لتقلّ الإطارات.","why":"التقطّع يأتي من عمل التخطيط في كل إطار؛ والانزلاق في 50ms أسرع من أن يتابعه الناس."},{"text":"استخدم مكتبة حركة بـ JavaScript بدل CSS.","why":"مكتبة JS تحرّك <code dir=\"ltr\">left</code> تصطدم بكلفة التخطيط نفسها، وتعمل على الـ main thread المشغول أيضًا."}],"answer":1},{"q":"<p>أيّ تغيير في الخصائص يسبّب عادةً إعادة رسم دون تخطيط؟</p>","options":[{"text":"<code dir=\"ltr\">padding</code>","why":"الـ padding يغيّر حجم صندوق العنصر، فيعمل التخطيط (ثم الرسم)."},{"text":"<code dir=\"ltr\">font-size</code>","why":"حجم الخط يغيّر مقاييس النص وفواصل الأسطر، وهذا تغيير في التخطيط."},{"text":"<code dir=\"ltr\">background-color</code>","why":"صحيح. الأبعاد لا تتغيّر فيُتخطّى التخطيط، لكن يجب إعادة رسم البكسلات."}],"answer":2},{"q":"<p>في الجدول 60 بطاقة جلسة على مدى يومين، والرسم الأول بطيء. أضفت <code dir=\"ltr\">content-visibility: auto</code> إلى قسم كل يوم. ماذا يجب أن تضيف أيضًا؟</p>","options":[{"text":"<code dir=\"ltr\">contain-intrinsic-size: auto 1200px</code>، لتحتفظ الأقسام المتخطّاة بارتفاع تقديري ولا يقفز شريط التمرير.","why":"صحيح. المحتوى المتخطّى ليس له حجم خاص به؛ وحجم العنصر النائب الذاتي (الذي يُتذكَّر بعد الرسم الأول بفضل <code dir=\"ltr\">auto</code>) يُبقي التمرير مستقرًا."},{"text":"<code dir=\"ltr\">will-change: contents</code> على كل بطاقة.","why":"هذا يطلب من المتصفّح أن يجهّز كل بطاقة للتغيير، ما يكلّف ذاكرة ويلغي التوفير."},{"text":"لا شيء؛ content-visibility تتولّى الأحجام تلقائيًا.","why":"دون تقدير للحجم تنهار الأقسام المتخطّاة إلى ارتفاع صفر، فتقفز الصفحة وشريط التمرير أثناء التمرير."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<section class=\"day\">\n  <h2>Day 1</h2>\n  <article class=\"speaker\">\n    <div class=\"photo\" role=\"img\" aria-label=\"Ana Ortiz\"></div>\n    <p>Ana Ortiz</p>\n  </article>\n</section>\n<section class=\"day\">\n  <h2>Day 2</h2>\n  <p>Twenty-eight sessions…</p>\n</section>\n<div class=\"toast\" role=\"status\">Saved to your schedule</div>","css":".speaker { width: 160px; }\n.photo {\n  width: 100%;\n  background: #ede9fe;\n  border-radius: 12px;\n}\n.toast {\n  position: fixed;\n  left: 16px;\n  bottom: 16px;\n  padding: 8px 14px;\n  border-radius: 8px;\n  background: #1f2937;\n  color: white;\n  animation: toast-in 250ms ease-out;\n}\n\n@keyframes toast-in {\n  from { left: -320px; }\n  to { left: 16px; }\n}\n"},"solution":{"html":"<section class=\"day\">\n  <h2>Day 1</h2>\n  <article class=\"speaker\">\n    <div class=\"photo\" role=\"img\" aria-label=\"Ana Ortiz\"></div>\n    <p>Ana Ortiz</p>\n  </article>\n</section>\n<section class=\"day\">\n  <h2>Day 2</h2>\n  <p>Twenty-eight sessions…</p>\n</section>\n<div class=\"toast\" role=\"status\">Saved to your schedule</div>","css":".speaker { width: 160px; }\n.photo {\n  width: 100%;\n  background: #ede9fe;\n  border-radius: 12px;\n}\n.toast {\n  position: fixed;\n  left: 16px;\n  bottom: 16px;\n  padding: 8px 14px;\n  border-radius: 8px;\n  background: #1f2937;\n  color: white;\n  animation: toast-in 250ms ease-out;\n}\n.photo { aspect-ratio: 1; }\n\n.day {\n  content-visibility: auto;\n  contain-intrinsic-size: auto 1200px;\n}\n\n@keyframes toast-in {\n  from { translate: -336px 0; opacity: 0; }\n  to { translate: 0 0; opacity: 1; }\n}\n"},"checks":[{"id":"toast-keyframes-cheap","test":"const all = []; const walk = (rules) => { for (const r of rules) { all.push(r); if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); const kf = all.find((r) => r instanceof CSSKeyframesRule && r.name === 'toast-in'); if (!kf) return false; const props = [...kf.cssRules].flatMap((f) => [...f.style]); const cheap = ['translate', 'transform', 'opacity', 'scale']; return props.length > 0 && props.every((p) => cheap.includes(p)) && props.some((p) => p === 'translate' || p === 'transform');"},{"id":"toast-still-animates","test":"return style('.toast', 'animation-name') === 'toast-in' && parseFloat(style('.toast', 'animation-duration')) > 0;"},{"id":"toast-final-position","test":"await tick(400); const r = $('.toast').getBoundingClientRect(); return Math.abs(r.left - 16) < 1;"},{"id":"skip-offscreen","test":"return $$('.day').every((d) => style(d, 'content-visibility') === 'auto');"},{"id":"intrinsic-size","test":"return $$('.day').every((d) => { const v = style(d, 'contain-intrinsic-size'); return v.includes('auto') && /\\d/.test(v); });"},{"id":"photo-reserves-space","test":"const r = $('.photo').getBoundingClientRect(); return r.width > 0 && Math.abs(r.height - r.width) < 1;"}],"title":"اجعل رسم الجدول رخيصًا","prompt":"<p>ثلاثة إصلاحات للأداء، كلها في CSS:</p>\n<ol>\n<li>أعد كتابة الـ keyframes <code dir=\"ltr\">toast-in</code> لينزلق الإشعار من اليسار باستخدام خصائص <strong>ملائمة للـ compositor</strong> فقط (<code dir=\"ltr\">translate</code> أو <code dir=\"ltr\">transform</code>، إضافةً إلى <code dir=\"ltr\">opacity</code> إن شئت). أبقِ الاسم <code dir=\"ltr\">toast-in</code>، وتأكّد من أن الإشعار ينتهي عند <code dir=\"ltr\">left: 16px</code>.</li>\n<li>دع المتصفّح يتخطّى رسم أقسام <code dir=\"ltr\">.day</code> الواقعة خارج الشاشة، مع <strong>تقدير للحجم الذاتي</strong> يستخدم <code dir=\"ltr\">auto</code> وطولًا.</li>\n<li>اجعل إطار <code dir=\"ltr\">.photo</code> <strong>مربّعًا</strong> قبل تحميل أيّ صورة، ليحجز مساحته.</li>\n</ol>\n","hints":["يمكن أن تبقى <code dir=\"ltr\">left: 16px</code> في الإشعار كما هي؛ حرّك بدلًا من ذلك مقدار إزاحته عن ذلك الموضع.","<code dir=\"ltr\">content-visibility: auto</code> تقترن بـ <code dir=\"ltr\">contain-intrinsic-size: auto &lt;length&gt;</code>.","<code dir=\"ltr\">@keyframes toast-in { from { translate: -336px 0; opacity: 0; } to { translate: 0 0; opacity: 1; } }</code> و<code dir=\"ltr\">.photo { aspect-ratio: 1; }</code>."],"explanation":"<p>تحريك <code dir=\"ltr\">left</code> يغيّر الأبعاد، فكان كل إطار يعيد تشغيل التخطيط والرسم. أما <code dir=\"ltr\">translate</code> و<code dir=\"ltr\">opacity</code> فلا تغيّران إلا طريقة تركيب طبقة الإشعار المرسومة أصلًا، فيستطيع المتصفّح تشغيل الحركة دون تخطيط أو رسم، وكثيرًا ما يكون ذلك خارج الـ main thread. و<code dir=\"ltr\">content-visibility: auto</code> تتيح للمتصفّح تخطّي أقسام الأيام الواقعة خارج الشاشة كليًا، و<code dir=\"ltr\">contain-intrinsic-size: auto 1200px</code> تنوب عن ارتفاعها حتى تُرسم مرة، فيبقى شريط التمرير مستقرًا. و<code dir=\"ltr\">aspect-ratio: 1</code> تعطي إطار الصورة ارتفاعًا مستمدًّا من عرضه، فلا يتزحزح شيء حين تصل الصورة.</p>\n","checkLabels":{"toast-keyframes-cheap":"toast-in لا تحرّك إلا translate/transform وopacity","toast-still-animates":"الإشعار ما زال يشغّل حركة toast-in","toast-final-position":"الإشعار ينتهي في موضعه الطبيعي","skip-offscreen":"أقسام الأيام تستخدم content-visibility: auto","intrinsic-size":"لأقسام الأيام تقدير للحجم الذاتي مع auto","photo-reserves-space":"إطار الصورة مربّع قبل تحميل الصورة"}},"words":682,"readMinutes":4}