أداء الحركات وتحسينها
لماذا يهم أداء الحركات
الحركة الجميلة التي تتلعثم أو تتجمد أو تجعل الصفحة تبدو بطيئة هي أسوأ من عدم وجود حركة على الإطلاق. يدرك المستخدمون التقطع -- التلعثم المرئي أو إسقاط الإطارات -- كعلامة على تطبيق معطل أو منخفض الجودة. لإنشاء حركات سلسة تعمل بمعدل 60 إطارا في الثانية (الهدف لمعظم الشاشات)، تحتاج لفهم كيف يعرض المتصفح المحتوى وأي خصائص CSS رخيصة أو مكلفة للتحريك.
يغوص هذا الدرس عميقا في خط أنابيب العرض بالمتصفح، ويشرح لماذا بعض الخصائص تتحرك بسلاسة بينما تسبب أخرى مشاكل في الأداء، ويمنحك أدوات وتقنيات عملية لتشخيص وإصلاح مشاكل أداء الحركات. بنهاية هذا الدرس، سيكون لديك نموذج ذهني واضح لما يحدث خلف الكواليس عند تشغيل حركات CSS، وقائمة مراجعة لضمان أن حركاتك دائما ناعمة كالزبدة.
خط أنابيب العرض بالمتصفح
في كل مرة يحتاج المتصفح لعرض شيء على الشاشة، يمر بسلسلة من الخطوات تسمى خط أنابيب العرض (المعروف أيضا بخط أنابيب البكسل). فهم هذه الخطوات هو المفتاح لفهم أداء الحركات. كل خطوة لها تكلفة، والحركات التي تُشغل خطوات أكثر تكون أكثر تكلفة.
الخطوة 1: التحليل (HTML وCSS)
يقرأ المتصفح HTML الخاص بك ويبني شجرة DOM (نموذج كائن المستند). كما يحلل CSS ويُنشئ CSSOM (نموذج كائن CSS). تمثل هاتان الشجرتان بنية مستندك والأنماط التي تنطبق على كل عنصر. تحدث هذه الخطوة مرة واحدة عند تحميل الصفحة ومرة أخرى كلما تغير DOM أو الأنماط. بالنسبة للحركات، ليست خطوة التحليل عادة عنق الزجاجة لأن بنية DOM لا تتغير أثناء حركة CSS.
الخطوة 2: الأنماط (إعادة حساب الأنماط)
يجمع المتصفح بين DOM وCSSOM لمعرفة قواعد CSS التي تنطبق على كل عنصر وما هي القيم المحسوبة النهائية. هذا يسمى إعادة حساب الأنماط. عندما تغير حركة خاصية CSS، يجب على المتصفح إعادة حساب الأنماط للعنصر المتأثر وربما لأحفاده. تعتمد تكلفة هذه الخطوة على عدد العناصر المتأثرة بتغيير النمط ومدى تعقيد محددات CSS الخاصة بك.
الخطوة 3: التخطيط (إعادة التدفق)
بمجرد أن يعرف المتصفح أنماط كل عنصر، يحسب الهندسة: الموضع والحجم وكيف ترتبط العناصر ببعضها مكانيا. هذه الخطوة تسمى التخطيط أو إعادة التدفق. التخطيط مكلف لأن تغيير حجم أو موضع عنصر واحد يمكن أن يتسلسل ويؤثر على تخطيط العديد من العناصر الأخرى. إذا غيرت حركتك خصائص مثل width أو height أو top أو left أو margin أو padding، يجب على المتصفح إعادة حساب التخطيط في كل إطار من الحركة.
الخطوة 4: الرسم
بعد التخطيط، يملأ المتصفح البكسلات. يرسم النص والألوان والصور والحدود والظلال -- كل جزء مرئي من كل عنصر. عمليات الرسم تملأ مخازن البكسلات (الصور النقطية) التي تمثل المحتوى المرئي لكل طبقة. الرسم مكلف بشكل معتدل ويُشغّل عندما تغير خصائص مرئية لا تؤثر على التخطيط، مثل background-color أو color أو box-shadow أو border-color.
الخطوة 5: التركيب
أخيرا، يأخذ المتصفح الطبقات المرسومة ويجمعها بالترتيب الصحيح لإنتاج الصورة النهائية على الشاشة. هذا يسمى التركيب. يمكن للمركب التعامل مع عمليات معينة بكفاءة عالية لأنه يعمل مع طبقات مرسومة بالفعل -- فقط يحتاج لنقل أو تدوير أو تكبير أو تغيير شفافية هذه الطبقات. لهذا السبب transform وopacity هما أرخص الخصائص للتحريك: يُشغلان فقط خطوة التركيب، متخطيين التخطيط والرسم بالكامل.
خطوات خط أنابيب العرض
/* خط الأنابيب الكامل (الأكثر تكلفة):
التحليل ← الأنماط ← التخطيط ← الرسم ← التركيب
الخصائص التي تُشغل التخطيط (الأكثر تكلفة):
width, height, top, left, right, bottom,
margin, padding, border-width, display,
position, float, font-size, font-weight,
line-height, text-align, overflow, flex, grid
الخصائص التي تُشغل الرسم (تكلفة متوسطة):
background-color, color, box-shadow, border-color,
border-style, border-radius, outline, visibility,
text-decoration, background-image
الخصائص التي تُشغل التركيب فقط (الأرخص):
transform, opacity, filter (في بعض المتصفحات),
will-change, perspective */
خصائص المركب فقط: transform وopacity
القاعدة الأهم لأداء الحركات هي: كلما أمكن، حرك فقط transform وopacity. هاتان الخاصيتان مميزتان لأن المتصفح يمكنه التعامل معهما بالكامل على مسار المركب، الذي يعمل بشكل منفصل عن المسار الرئيسي. هذا يعني أن حركاتك يمكن أن تبقى سلسة حتى لو كان المسار الرئيسي مشغولا بتنفيذ JavaScript أو معالجة DOM أو مهام أخرى.
لماذا transform قوية جدا
يمكن لخاصية transform أن تحل محل معظم الحركات المُشغلة للتخطيط. بدلا من تحريك top أو left لنقل عنصر، استخدم translateX() أو translateY(). بدلا من تحريك width أو height لتغيير الحجم، استخدم scale(). النتيجة المرئية غالبا متطابقة، لكن فرق الأداء هائل.
سيء مقابل جيد: نقل عنصر
/* سيء: تحريك top/left يُشغل التخطيط في كل إطار */
@keyframes moveBoxBad {
from {
top: 0;
left: 0;
}
to {
top: 200px;
left: 300px;
}
}
.box-bad {
position: absolute;
animation: moveBoxBad 1s ease-out forwards;
/* يُشغل: الأنماط ← التخطيط ← الرسم ← التركيب (كل إطار) */
}
/* جيد: تحريك transform يُشغل التركيب فقط */
@keyframes moveBoxGood {
from {
transform: translate(0, 0);
}
to {
transform: translate(300px, 200px);
}
}
.box-good {
animation: moveBoxGood 1s ease-out forwards;
/* يُشغل: التركيب فقط (كل إطار) -- أسرع بكثير! */
}
سيء مقابل جيد: تغيير حجم عنصر
/* سيء: تحريك width/height يُشغل التخطيط */
@keyframes growBad {
from {
width: 100px;
height: 100px;
}
to {
width: 200px;
height: 200px;
}
}
.grow-bad {
animation: growBad 0.5s ease-out;
/* التخطيط يُعاد حسابه كل إطار، قد يؤثر على العناصر المجاورة */
}
/* جيد: تحريك scale يُشغل التركيب فقط */
@keyframes growGood {
from {
transform: scale(1);
}
to {
transform: scale(2);
}
}
.grow-good {
animation: growGood 0.5s ease-out;
/* لا إعادة حساب للتخطيط -- فقط المركب يعمل */
}
transform: scale() أكثر أداء بكثير من تحريك width/height، إلا أنهما لا ينتجان نتائج متطابقة. التكبير يمدد المحتوى المعروض (بما في ذلك النص والحدود)، بينما تغيير العرض/الارتفاع يجعل المتصفح يعيد التخطيط والعرض بالحجم الجديد. لمعظم حركات واجهة المستخدم مثل تأثيرات التمرير وحركات الدخول، الفرق غير محسوس. لكن للعناصر الغنية بالنص أو ذات الحدود المعقدة، قد تلاحظ فرقا مرئيا.قوة opacity
تغييرات الشفافية يتعامل معها المركب بالكامل لأنها تؤثر فقط على كيفية مزج الطبقات معا. تلاشي العناصر دخولا وخروجا هو من أرخص الحركات التي يمكنك إجراؤها. إذا كنت بحاجة لإخفاء وإظهار عناصر بالحركة، فضل دائما تحريك opacity على visibility أو display.
حركات تلاشي فعالة
/* جيد: الشفافية للمركب فقط */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* ادمج transform وopacity للحصول على أفضل أداء */
@keyframes slideInOptimized {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* هذه الحركة تُشغل التركيب فقط -- أقصى أداء */
.optimized-entrance {
animation: slideInOptimized 0.4s ease-out both;
}
لماذا تحريك خصائص التخطيط مكلف
عندما تحرك خاصية تُشغل التخطيط (مثل width أو height أو margin أو top أو left)، يجب على المتصفح تنفيذ العمل التالي في كل إطار حركة (مثاليا 60 مرة في الثانية):
- إعادة حساب الأنماط للعنصر وربما لأسلافه وأحفاده.
- تشغيل التخطيط لتحديد الهندسة الجديدة للعنصر وكل شيء يؤثر فيه. تغيير في عرض عنصر واحد يمكن أن يزيح العناصر المجاورة، ويغير ارتفاع الأب، ويُشغل إعادة حساب أشرطة التمرير، ويتسلسل عبر المستند بأكمله.
- إعادة رسم كل منطقة متأثرة من الشاشة.
- تركيب الطبقات المحدثة.
كل هذا العمل يحدث على المسار الرئيسي، المسؤول أيضا عن تشغيل JavaScript ومعالجة مدخلات المستخدم ومعالجة استجابات الشبكة. إذا استغرق حساب التخطيط أكثر من حوالي 16 مللي ثانية (ميزانية إطار واحد عند 60 إطارا في الثانية)، يسقط المتصفح إطارا ويرى المستخدم تقطعا.
حركات مُشغلة للتخطيط شائعة يجب تجنبها
/* تجنب: كل هذه تُشغل التخطيط في كل إطار */
/* تحريك الأبعاد */
@keyframes expandWidth {
to { width: 100%; } /* يُشغل التخطيط */
}
/* تحريك خصائص الموضع */
@keyframes slideDown {
to { top: 100px; } /* يُشغل التخطيط */
}
/* تحريك الهوامش */
@keyframes pushDown {
to { margin-top: 50px; } /* يُشغل التخطيط، يدفع العناصر الأخرى */
}
/* تحريك الحشو */
@keyframes padIn {
to { padding: 30px; } /* يُشغل التخطيط، يغير منطقة المحتوى */
}
/* تحريك عرض الحد */
@keyframes borderGrow {
to { border-width: 10px; } /* يُشغل التخطيط */
}
/* بدائل أفضل باستخدام transform: */
/* expandWidth → استخدم scaleX() */
/* slideDown → استخدم translateY() */
/* pushDown → استخدم translateY() (لكن هذا لن يدفع العناصر المجاورة) */
/* padIn → حدد حجم العنصر مسبقا واستخدم scale أو opacity */
/* borderGrow → استخدم box-shadow أو outline بدلا من ذلك */
خاصية will-change
خاصية will-change هي تلميح للمتصفح بأن خصائص معينة لعنصر من المحتمل أن تتغير في المستقبل القريب. عندما تعلن will-change، يمكن للمتصفح إعداد التحسينات مقدما -- مثل ترقية العنصر لطبقة مركب خاصة به أو تخصيص ذاكرة مسبقا للحركة. هذا يمكن أن يلغي تكلفة الإعداد التي ستحدث عادة عند بدء الحركة.
استخدام will-change بشكل صحيح
/* أخبر المتصفح أن هذا العنصر سيتحرك */
.card {
will-change: transform, opacity;
}
/* أفضل: طبق will-change فقط عند الحاجة */
.card-container:hover .card {
will-change: transform;
}
.card-container:hover .card {
animation: slideUp 0.3s ease-out forwards;
}
/* أو طبق will-change قبل بدء الحركة مباشرة */
.card.about-to-animate {
will-change: transform, opacity;
}
.card.animating {
animation: slideInOptimized 0.5s ease-out both;
}
.card.animation-done {
will-change: auto; /* إزالة التلميح بعد الحركة */
}
will-change على كل عنصر أو تستخدمه كحل شامل للأداء. كل تصريح will-change يجعل المتصفح يخصص ذاكرة إضافية وينشئ طبقة مركب جديدة. الكثير من الطبقات تستهلك ذاكرة GPU، ويمكن أن تُضعف الأداء فعليا، وقد تسبب عيوبا مرئية. استخدم will-change فقط على العناصر التي تعرف أنها ستتحرك، ويفضل إزالته بعد اكتمال الحركة.متى تستخدم will-change
النمط المثالي لاستخدام will-change هو تطبيقه قبل بدء الحركة بقليل وإزالته بعد انتهاء الحركة. هذا يمنح المتصفح وقتا لإعداد التحسين دون استهلاك الموارد بشكل دائم.
أنماط will-change عملية
/* النمط 1: التطبيق عند تمرير الأب */
.gallery-item:hover .thumbnail {
will-change: transform;
transform: scale(1.1);
transition: transform 0.3s ease;
}
/* النمط 2: التطبيق بـ JavaScript قبل الحركة */
نمط JavaScript لـ will-change
// تطبيق will-change قبل الحركة
function animateElement(el) {
el.style.willChange = 'transform, opacity';
// تأخير صغير للسماح للمتصفح بإعداد التحسين
requestAnimationFrame(() => {
el.classList.add('animate-in');
});
// إزالة will-change بعد اكتمال الحركة
el.addEventListener('animationend', () => {
el.style.willChange = 'auto';
}, { once: true });
}
أنماط مضادة: الإفراط في استخدام will-change
/* سيء: تطبيق will-change على كل شيء */
* {
will-change: transform, opacity;
/* هذا ينشئ طبقات لكل عنصر -- هدر ذاكرة هائل */
}
/* سيء: خصائص كثيرة جدا */
.element {
will-change: transform, opacity, top, left, width, height,
background-color, box-shadow, border-radius;
/* الإفراط في التلميح يفشل الغرض */
}
/* سيء: will-change دائم على عناصر نادرا ما تتحرك */
.static-header {
will-change: transform;
/* هذا العنوان لا يتحرك فعليا أبدا -- موارد مهدرة */
}
/* جيد: مستهدف ومؤقت */
.modal.opening {
will-change: transform, opacity;
}
.modal.open {
will-change: auto;
}
خاصية contain
تخبر خاصية contain المتصفح أن عنصرا ومحتوياته مستقلة عن بقية المستند بطرق محددة. هذا يسمح للمتصفح بالتحسين عن طريق تحديد نطاق حسابات العرض. عندما تحرك عناصر داخل منطقة محتواة، يعرف المتصفح أن التغييرات لا يمكن أن تؤثر على العناصر خارج تلك المنطقة، لذا يمكنه تخطي إعادة حساب التخطيط أو الرسم لبقية الصفحة.
استخدام خاصية contain
/* احتواء التخطيط: التخطيط الداخلي للعنصر
لا يؤثر على بقية الصفحة */
.widget {
contain: layout;
}
/* احتواء الرسم: المحتوى المرئي للعنصر
لا يُرسم خارج حدوده */
.animated-card {
contain: paint;
}
/* احتواء الحجم: حجم العنصر يُحدد
بشكل مستقل عن أبنائه */
.fixed-panel {
contain: size;
width: 300px;
height: 200px;
}
/* احتواء صارم: التخطيط + الرسم + الحجم */
.fully-contained {
contain: strict;
width: 100%;
height: 400px;
}
/* احتواء المحتوى: التخطيط + الرسم (الأكثر فائدة شيوعا) */
.contained-section {
contain: content;
}
/* عملي: احتواء منطقة الحركة لتحديد تأثيرها على الأداء */
.animation-container {
contain: content;
/* الآن تغييرات التخطيط/الرسم داخل هذا العنصر
لن تُشغل إعادة حساب خارجه */
}
.animation-container .animated-item {
animation: complexAnimation 2s ease infinite;
}
contain: content (التي تجمع بين احتواء التخطيط والرسم) هي الخيار الأكثر أمانا والأكثر فائدة شيوعا. لا تتطلب منك تعيين أبعاد صريحة (على عكس contain: size) لكنها لا تزال توفر فوائد تحسين كبيرة عن طريق عزل عرض العنصر عن بقية الصفحة.تقليل إرهاق التخطيط
يحدث إرهاق التخطيط عندما يقرأ JavaScript بشكل متكرر خصائص التخطيط ثم يكتب تغييرات الأنماط بطريقة متداخلة، مما يجبر المتصفح على إعادة حساب التخطيط عدة مرات بشكل متزامن. بينما هذا في الأساس مشكلة JavaScript، إلا أنه يؤثر مباشرة على أداء الحركات لأنه يمكن أن يحرم المسار الرئيسي من الوقت اللازم لمعالجة حركات CSS.
إرهاق التخطيط: المشكلة
/* JavaScript يسبب إرهاق التخطيط */
// سيء: نمط قراءة-كتابة-قراءة-كتابة يفرض حسابات تخطيط متعددة
function updateElements(elements) {
elements.forEach(el => {
const height = el.offsetHeight; // قراءة (تفرض التخطيط)
el.style.height = (height * 2) + 'px'; // كتابة (تبطل التخطيط)
// التكرار التالي: القراءة تفرض التخطيط مرة أخرى!
});
}
// جيد: تجميع القراءات، ثم تجميع الكتابات
function updateElementsBetter(elements) {
// المرور الأول: قراءة جميع القيم
const heights = elements.map(el => el.offsetHeight);
// المرور الثاني: كتابة جميع القيم
elements.forEach((el, i) => {
el.style.height = (heights[i] * 2) + 'px';
});
// إعادة حساب تخطيط واحدة فقط!
}
الخصائص التي تفرض التخطيط عند القراءة
/* قراءة أي من هذه الخصائص تجبر المتصفح
على حساب التخطيط بشكل متزامن (إذا كان متسخا):
خصائص العنصر:
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop, scrollLeft, scrollWidth, scrollHeight
- clientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle() (للخصائص المتعلقة بالتخطيط)
- getBoundingClientRect()
خصائص النافذة:
- window.scrollX, window.scrollY
- window.innerWidth, window.innerHeight
أفضل ممارسة: تجنب قراءة هذه الخصائص داخل
حلقات الحركة أو معالجات الأحداث السريعة. إذا كان يجب
قراءتها، خزن القيم واقرأها جميعا دفعة واحدة
قبل إجراء أي تغييرات في الأنماط. */
requestAnimationFrame مقابل حركات CSS
متى يجب استخدام حركات CSS ومتى يجب استخدام JavaScript مع requestAnimationFrame؟ لكل نهج نقاط قوة والاختيار الصحيح يعتمد على حالة الاستخدام المحددة.
حركات CSS: نقاط القوة
- محسنة للمركب: حركات CSS لـ
transformوopacityيمكن أن تعمل بالكامل على مسار المركب، مستقلة عن المسار الرئيسي. هذا يعني أنها تستمر في العمل بسلاسة حتى عندما يكون JavaScript مشغولا. - تصريحية: تصف ما يجب أن يحدث والمتصفح يتعامل مع الكيفية. يمكن للمتصفح تطبيق تحسيناته الخاصة.
- لا تحتاج JavaScript: بايتات أقل للتنزيل والتحليل. لا خطر من أخطاء JavaScript تكسر الحركة.
- مسرعة بالـ GPU افتراضيا: المتصفح يرقي تلقائيا العناصر المتحركة لطبقات GPU عند استخدام transform وopacity.
requestAnimationFrame: نقاط القوة
- تحكم ديناميكي: يمكنك تغيير معلمات الحركة بناء على مدخلات المستخدم أو حسابات الفيزياء أو البيانات الفورية.
- تنسيق معقد: تنسيق عناصر كثيرة ذات توقيت مترابط أسهل في JavaScript.
- Canvas وWebGL: لحركات canvas أو ثلاثية الأبعاد، JavaScript هو الخيار الوحيد.
- توقيت دقيق: لديك وصول للطابع الزمني الدقيق لكل إطار للحركات القائمة على الفيزياء.
متى تختار أي نهج
/* استخدم حركات CSS لـ:
- انتقالات الحالة البسيطة (تأثيرات التمرير، مداخل الصفحة)
- مؤشرات التحميل الدوارة والحركات المتكررة
- تسلسلات الحركة المحددة مسبقا
- الحركات التي يجب أن تعمل أثناء عمليات JavaScript الثقيلة
- حركات الكشف عند التمرير (مع Intersection Observer)
*/
/* استخدم requestAnimationFrame لـ:
- الحركات التي تعتمد على مدخلات المستخدم (موضع الماوس، التمرير)
- الحركات القائمة على الفيزياء (النابض، الجاذبية، الزخم)
- عرض Canvas/WebGL
- الحركات التي تحتاج للتنسيق مع منطق JavaScript
- حلقات الألعاب
*/
/* مثال: حركة CSS لدخول بسيط */
.card {
animation: fadeInUp 0.5s ease-out both;
}
/* مثال: requestAnimationFrame لمتابعة المؤشر */
مثال requestAnimationFrame
// حركة متابعة المؤشر (غير ممكنة بـ CSS وحده)
const follower = document.querySelector('.cursor-follower');
let mouseX = 0, mouseY = 0;
let followerX = 0, followerY = 0;
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
function animate() {
// استيفاء سلس نحو موضع الماوس
followerX += (mouseX - followerX) * 0.1;
followerY += (mouseY - followerY) * 0.1;
// استخدم transform للأداء!
follower.style.transform =
`translate(${followerX}px, ${followerY}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
التسريع العتادي وطبقات GPU
عندما يُرقّى عنصر لطبقة مركب خاصة به، يتولى GPU (وحدة معالجة الرسومات) عرضه بدلا من CPU. يتفوق GPU في عمليات مثل نقل وتدوير وتكبير وضبط شفافية القوام (الصور النقطية) -- وهذا بالضبط ما يحدث أثناء حركات transform وopacity. لهذا يمكن لهذه الحركات العمل بمعدل 60 إطارا في الثانية حتى على أجهزة متواضعة.
ما يسبب ترقية الطبقة
ينشئ المتصفح تلقائيا طبقات مركب جديدة للعناصر في عدة حالات:
- العناصر مع
will-change: transformأوwill-change: opacity - العناصر التي تُحرك حاليا بـ
transformأوopacity - العناصر مع تحويلات ثلاثية الأبعاد (
translate3d،rotate3d،perspective) - العناصر مع فلاتر CSS معينة
- عناصر
<video>و<canvas> - العناصر التي تتداخل مع طبقات مركبة أخرى (ترقية ضمنية)
تشغيل تسريع GPU
/* المتصفح يرقي هذه تلقائيا لطبقات GPU */
/* حركة CSS نشطة على transform */
.animated {
animation: slideIn 0.5s ease-out;
}
/* تلميح will-change */
.about-to-animate {
will-change: transform;
}
/* تحويل ثلاثي الأبعاد (حيلة كلاسيكية -- الآن فضل will-change) */
.gpu-layer {
transform: translateZ(0);
/* أو */
transform: translate3d(0, 0, 0);
/* هذه تفرض طبقة GPU، لكن will-change هو النهج الحديث */
}
/* ملاحظة: حيلة translateZ(0) لا تزال تعمل وتُستخدم على نطاق واسع،
لكن will-change أكثر صحة دلاليا ويعطي المتصفح
معلومات أكثر حول ما يجب تحسينه. */
استخدام DevTools لتشخيص الأداء
توفر أدوات المطور الحديثة في المتصفحات أدوات قوية لتشخيص مشاكل أداء الحركات. تعلم استخدام هذه الأدوات ضروري لأي مطور يعمل مع الحركات.
تبويب الأداء
يتيح لك تبويب الأداء (في أدوات Chrome للمطورين) تسجيل جدول زمني لكل شيء يفعله المتصفح أثناء تشغيل حركتك. يمكنك رؤية بالضبط كم يستغرق كل إطار، وأي مراحل من خط أنابيب العرض تعمل، وما إذا كانت أي إطارات تتجاوز ميزانية 16 مللي ثانية.
كيفية استخدام تبويب الأداء
/* خطوات تحليل حركة:
1. افتح أدوات Chrome للمطورين (F12 أو Cmd+Opt+I)
2. انتقل لتبويب الأداء (Performance)
3. انقر زر التسجيل (أيقونة الدائرة)
4. شغّل حركتك على الصفحة
5. انتظر اكتمال الحركة
6. انقر إيقاف لإنهاء التسجيل
ما تبحث عنه في التسجيل:
- الأشرطة الخضراء: عمليات الرسم
- الأشرطة البنفسجية: عمليات التخطيط
- الأشرطة الصفراء: تنفيذ JavaScript
- مخطط معدل الإطارات: يجب أن يكون ثابتا عند ~60 fps
- المثلثات الحمراء: تشير لإطارات مُسقطة (تقطع)
إذا رأيت الكثير من الأشرطة البنفسجية (التخطيط) أثناء الحركة:
→ أنت تحرك خصائص مُشغلة للتخطيط
→ انتقل لـ transform وopacity
إذا رأيت الكثير من الأشرطة الخضراء (الرسم) أثناء الحركة:
→ أنت تحرك خصائص مُشغلة للرسم
→ فكر إذا كان transform يمكنه تحقيق نفس التأثير
إذا انخفض معدل الإطارات عن 60 fps:
→ تحقق من مراحل العرض التي تستغرق وقتا طويلا
→ قلل تعقيد الخصائص المتحركة */
لوحة الطبقات
تعرض لك لوحة الطبقات جميع طبقات المركب على الصفحة، وأحجامها، واستهلاك ذاكرتها، وسبب إنشائها. هذا لا يُقدر بثمن لفهم أي العناصر مسرعة بالـ GPU وما إذا كان لديك طبقات كثيرة جدا.
استخدام لوحة الطبقات
/* خطوات عرض طبقات المركب:
Chrome:
1. افتح أدوات المطور ← المزيد من الأدوات ← الطبقات
2. أو: افتح أدوات المطور ← تبويب العرض ← حدد "حدود الطبقات"
(يعرض حدودا ملونة حول طبقات المركب على الصفحة)
ما يجب التحقق منه:
- عدد الطبقات الموجودة (أقل أفضل عموما)
- إجمالي الذاكرة المستهلكة بجميع الطبقات
- سبب إنشاء كل طبقة (will-change، transform، إلخ)
- ما إذا كانت عناصر غير متوقعة قد رُقيت لطبقات
Firefox:
1. افتح أدوات المطور ← الإعدادات ← فعّل "تبديل وميض الرسم"
(يُظهر المناطق التي يُعاد رسمها باللون الأخضر)
غطاء وميض الرسم مفيد بشكل خاص:
- الومضات الخضراء = مناطق يُعاد رسمها
- أثناء حركة محسنة، لا يجب أن ترى ومضات خضراء
- إذا رأيت ومضات خضراء في كل إطار، فإن الحركة
تُشغل الرسم ويجب تحسينها */
أغطية لوحة العرض
أغطية العرض في أدوات Chrome للمطورين
/* الوصول: أدوات المطور ← قائمة النقاط الثلاث ← المزيد من الأدوات ← العرض
أغطية مفيدة:
1. وميض الرسم:
يعرض مستطيلات خضراء فوق المناطق التي يُعاد رسمها.
أثناء حركة transform/opacity، لا شيء يجب أن يومض.
2. مناطق تحول التخطيط:
يعرض مستطيلات زرقاء فوق المناطق التي تشهد تحولات تخطيط.
مفيد لاكتشاف الحركات التي تسبب عدم استقرار التخطيط.
3. عداد FPS:
يعرض عداد FPS فوري ورسم بياني لتوقيت الإطارات.
الهدف: خط ثابت عند 60 fps.
4. مشاكل أداء التمرير:
يُظهر العناصر التي تبطئ التمرير.
ذات صلة بالحركات المرتبطة بالتمرير.
5. مؤشرات الويب الأساسية:
تراقب LCP وFID وCLS في الوقت الفعلي.
CLS (تحول التخطيط التراكمي) يتأثر مباشرة
بالحركات المُشغلة للتخطيط. */
قائمة مراجعة الحركة بدون تقطع
استخدم هذه القائمة في كل مرة تنشئ فيها حركة لضمان تشغيلها بسلاسة عبر جميع الأجهزة. اتباع هذه القواعد سيمنع الغالبية العظمى من مشاكل أداء الحركات.
قائمة المراجعة الكاملة بدون تقطع
/* 1. حرك فقط TRANSFORM وOPACITY
هذه الخصائص الوحيدة التي تعمل على مسار المركب.
✓ transform: translate(), scale(), rotate(), skew()
✓ opacity
✗ width, height, top, left, margin, padding, border-width */
/* 2. استخدم will-change بحذر
طبقه قبل بدء الحركة، أزله بعد انتهائها.
لا تطبقه أبدا على أكثر من حفنة من العناصر. */
.about-to-animate { will-change: transform, opacity; }
.done-animating { will-change: auto; }
/* 3. احتوِ حركاتك
استخدم خاصية contain لتحديد نطاق العرض. */
.animation-area {
contain: content;
}
/* 4. تجنب التحريك أثناء التمرير
معالجات التمرير + الحركة = تقطع.
استخدم Intersection Observer بدلا من أحداث التمرير.
استخدم حركات CSS المدفوعة بالتمرير حيث مدعومة. */
/* 5. حافظ على الطبقات بالحد الأدنى
كل طبقة GPU تكلف ذاكرة.
لا ترقِ العناصر لطبقات بدون داع.
تحقق من لوحة الطبقات للتحقق من عدد الطبقات. */
/* 6. اختبر على أجهزة حقيقية
حركة سلسة على جهاز المطور الخاص بك قد
تتلعثم على هاتف متوسط المدى. اختبر دائما على:
- هاتف أندرويد متوسط المدى
- جهاز لوحي قديم
- كمبيوتر محمول بمعالج رسومات مدمج */
/* 7. احترم تفضيلات تقليل الحركة */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
/* 8. حدد الحركات المتزامنة
حركات متزامنة أكثر = عمل أكثر لكل إطار.
وزع الحركات بدلا من تشغيلها جميعا معا.
أبقِ العدد الإجمالي للعناصر المتحركة أقل من 20. */
/* 9. استخدم مُدد مناسبة
الدخول: 200-500 مللي ثانية (المستخدمون يتوقعون استجابات سريعة)
الخروج: 150-300 مللي ثانية (يجب أن تبدو أسرع قليلا من الدخول)
المتكررة/المحيطة: 1000-3000 مللي ثانية (يجب أن تبدو مسترخية) */
/* 10. تجنب إرهاق التخطيط في JS
اجمع قراءات DOM قبل كتابات DOM.
استخدم requestAnimationFrame للحركات المدفوعة بـ JS.
لا تقرأ أبدا خصائص التخطيط داخل حلقات الحركة. */
أمثلة تحسين عملية
دعنا نلقي نظرة على سيناريوهات واقعية حيث تحدث مشاكل أداء الحركات بشكل شائع وكيفية إصلاحها.
تحسين تأثير تمرير البطاقة
قبل وبعد التحسين
/* قبل: عدة مُشغلات للتخطيط والرسم */
.card {
transition: all 0.3s ease;
/* "all" تنقل كل شيء يتغير، بما في ذلك
الخصائص المُشغلة للتخطيط */
}
.card:hover {
margin-top: -10px; /* مُشغل تخطيط */
box-shadow: 0 10px 30px rgba(0,0,0,0.2); /* مُشغل رسم */
border-color: #3498db; /* مُشغل رسم */
padding: 22px; /* مُشغل تخطيط */
}
/* بعد: مركب فقط مع رسم أدنى */
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
/* انقل خصائص محددة فقط */
}
.card:hover {
transform: translateY(-10px); /* مركب فقط */
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
/* box-shadow يُشغل الرسم، لكنه لا مفر منه هنا.
على الأقل ألغينا مُشغلات التخطيط. */
}
تحسين تسلسل دخول الصفحة
دخول متدرج محسن
/* استخدم فقط transform وopacity لحركات الدخول */
@keyframes optimizedEntrance {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* توزيع بتأخيرات، حدد إجمالي العناصر المتحركة */
.section-content > * {
opacity: 0;
animation: optimizedEntrance 0.4s ease-out both;
}
.section-content > *:nth-child(1) { animation-delay: 0.05s; }
.section-content > *:nth-child(2) { animation-delay: 0.10s; }
.section-content > *:nth-child(3) { animation-delay: 0.15s; }
.section-content > *:nth-child(4) { animation-delay: 0.20s; }
.section-content > *:nth-child(5) { animation-delay: 0.25s; }
/* توقف هنا -- تحريك أكثر من ~10 عناصر في وقت واحد
يمكن أن يسبب إسقاط إطارات على الأجهزة المحمولة */
/* استخدم contain على الأب لتحديد نطاق العرض */
.section-content {
contain: content;
}
تحسين الهيكل التحميلي
تأثير لمعان محسن الأداء
/* سيء: تحريك background-position يُشغل الرسم */
@keyframes shimmerBad {
to { background-position: 200% 0; }
/* هذا يعمل لكنه يعيد الرسم في كل إطار */
}
/* أفضل: استخدم عنصر زائف مع transform */
@keyframes shimmerGood {
from { transform: translateX(-100%); }
to { transform: translateX(100%); }
}
.skeleton-optimized {
position: relative;
overflow: hidden;
background: #e0e0e0;
}
.skeleton-optimized::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: shimmerGood 1.5s ease-in-out infinite;
/* قائم على Transform: مركب فقط، بدون إعادة رسم! */
}
تحسين الحركات المُشغلة بالتمرير
استخدام Intersection Observer بدلا من أحداث التمرير
/* CSS: تعريف الحركة */
@keyframes revealOnScroll {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.scroll-reveal {
opacity: 0; /* مخفي افتراضيا */
}
.scroll-reveal.visible {
animation: revealOnScroll 0.6s ease-out both;
}
JavaScript: Intersection Observer
// جيد: Intersection Observer (خارج المسار الرئيسي، بدون تقطع تمرير)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
document.querySelectorAll('.scroll-reveal').forEach(el => {
observer.observe(el);
});
// سيء: مستمع حدث التمرير (يُطلق عند كل تمرير، يحجب المسار الرئيسي)
// window.addEventListener('scroll', () => {
// elements.forEach(el => {
// if (isInViewport(el)) el.classList.add('visible');
// });
// });
قياس أداء الحركات برمجيا
بعيدا عن الفحص المرئي باستخدام أدوات المطور، يمكنك قياس أداء الحركات برمجيا باستخدام Performance API وPerformanceObserver.
مراقبة معدل الإطارات بـ JavaScript
// عداد FPS بسيط
let frameCount = 0;
let lastTime = performance.now();
function measureFPS() {
frameCount++;
const currentTime = performance.now();
if (currentTime - lastTime >= 1000) {
console.log(`FPS: ${frameCount}`);
frameCount = 0;
lastTime = currentTime;
}
requestAnimationFrame(measureFPS);
}
requestAnimationFrame(measureFPS);
// مراقبة الإطارات الطويلة (تقطع محتمل)
const longFrameObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 16.67) {
console.warn(
`إطار طويل مُكتشف: ${entry.duration.toFixed(1)}ms`,
entry
);
}
}
});
longFrameObserver.observe({ type: 'long-animation-frame', buffered: true });
التمرين 1: تحسين حركة ضعيفة الأداء
خذ الحركة التالية الضعيفة التحسين وأعد كتابتها لتكون صديقة للمركب. الحركة الأصلية تنقل بطاقة لأعلى بمقدار 20 بكسل، وتزيد عرضها من 300px إلى 350px، وتضيف حشوة من 16px إلى 24px، وتغير لون خلفيتها من أبيض إلى أزرق فاتح، وتضيف ظل صندوق، وتزيد الحد من 1px إلى 3px -- كل ذلك عند التمرير مع transition: all 0.3s ease. أعد كتابة هذه الحركة لتحقيق نفس التأثير المرئي (أو أقرب ما يمكن) باستخدام فقط transform وopacity وخاصية واحدة على الأكثر مُشغلة للرسم (box-shadow). استخدم will-change بشكل مناسب بتطبيقه عند تمرير الحاوية الأب وإزالته عند انتهاء التمرير. أضف contain: content لحاوية البطاقة لتحديد نطاق العرض. حلل كلا الإصدارين في تبويب الأداء بأدوات المطور وقارن عدد عمليات التخطيط والرسم لكل إطار. اكتب ملاحظاتك حول الفرق في توقيت الإطارات بين الإصدارين.
التمرين 2: بناء لوحة تحكم حركات محسنة الأداء
أنشئ صفحة لوحة تحكم بست بطاقات عنصر واجهة متحركة على الأقل تكشف عن نفسها مع تمرير المستخدم لأسفل. استخدم Intersection Observer لتشغيل الحركات بدلا من مستمعي أحداث التمرير. يجب أن يكون لكل بطاقة حركة دخول متدرجة تستخدم فقط transform وopacity، مع تأخيرات محسوبة بناء على موضع كل بطاقة في الشبكة. أضف عنصر خلفية متحرك باستمرار (مثل تدرج لوني بطيء الدوران أو جزيئات عائمة) يستخدم فقط خصائص صديقة للمركب. طبق contain: content على كل حاوية عنصر واجهة. استخدم will-change بشكل صحيح بإضافته عبر JavaScript قبل بدء حركة كل بطاقة مباشرة وإزالته عند انتهاء الحركة. أضف غطاء عداد FPS (باستخدام تقنية requestAnimationFrame من هذا الدرس) يعرض معدل الإطارات الحالي. أضف زر تبديل يتحول بين إصدار غير محسن (يستخدم حركات width وheight وtop وleft) والإصدار المحسن (يستخدم transform وopacity) بحيث يمكنك مقارنة فرق الأداء بصريا. أضف استعلام الوسائط prefers-reduced-motion لتعطيل أو تبسيط جميع الحركات للمستخدمين الذين يفضلون تقليل الحركة. اختبر الصفحة مع خنق CPU بمقدار 6x في أدوات المطور وتحقق من أن الإصدار المحسن يحافظ على 60 إطارا ثابتة في الثانية بينما الإصدار غير المحسن يسقط إطارات.