مقدمة
تريد أن يكون تطبيق الويب سريع الاستجابة وسلاسة عند تنفيذ الرسوم المتحركة والانتقالات وغيرها من تأثيرات واجهة المستخدم الصغيرة. إنّ التأكّد من أنّ هذه التأثيرات خالية من أيّ مشاكل يمكن أن يشكّل الفرق بين شعور "أصلي" أو شعور غير سلس وغير مصقول.
هذه هي المقالة الأولى في سلسلة من المقالات التي تتناول تحسين أداء العرض في المتصفّح. في البداية، سنتناول سبب صعوبة إنشاء صور متحركة سلسة والخطوات التي يجب اتّخاذها لتحقيق ذلك، بالإضافة إلى بعض أفضل الممارسات السهلة. تم تقديم العديد من هذه الأفكار في الأصل في "Jank Busters"، وهي محادثة قدّمتها أنا ونات دوكا في مؤتمر Google I/O (فيديو) هذا العام.
نقدّم لك ميزة "مزامنة الإطارات"
قد يكون هذا المصطلح مألوفًا للاعبين على أجهزة الكمبيوتر الشخصي، ولكنّه غير شائع على الويب: ما هو مزامنة الإطارات؟
شاشة هاتفك مثلاً، يتم إعادة تحميلها على فترات منتظمة، عادةً (ولكن ليس دائمًا) حوالي 60 مرة في الثانية. يشير "المزامنة الرأسية" إلى إنشاء لقطات جديدة فقط بين عمليات إعادة تحميل الشاشة. يمكنك اعتبار ذلك حالة تسابق بين العملية التي تكتب البيانات في مخزن الشاشة ونظام التشغيل الذي يقرأ هذه البيانات لوضعها على الشاشة. نريد أن تتغيّر محتويات اللقطات المخزّنة مؤقتًا بين عمليات إعادة التحميل هذه، وليس أثناءها، وإلا ستعرض الشاشة نصف لقطة ونصف لقطة أخرى، ما يؤدي إلى حدوث تمويه.
للحصول على صورة متحركة سلسة، يجب أن يكون إطار جديد جاهزًا في كل مرة تتم فيها إعادة تحميل الشاشة. وينتج عن ذلك تأثيران كبيران: توقيت عرض اللقطات (أي الوقت الذي يجب أن يكون فيه اللقطة جاهزة) وميزانية اللقطات (أي الوقت الذي يستغرقه المتصفّح لعرض اللقطة). لا يتوفّر لك سوى الوقت الذي تتطلّبه عملية تحديث الشاشة لإكمال عرض إطار واحد (حوالي 16 ملي ثانية على شاشة بمعدّل تحديث 60 هرتز)، ويجب بدء عرض الإطار التالي فور عرض الإطار الأخير على الشاشة.
التوقيت هو كل شيء: requestAnimationFrame
يستخدم العديد من مطوّري الويب setInterval
أو setTimeout
كل 16 ملي ثانية لإنشاء صور متحركة. يشكّل ذلك مشكلة لأسباب مختلفة (سنناقش المزيد من التفاصيل بعد قليل)، ولكن من الأمور التي تثير القلق بشكل خاص ما يلي:
- لا تزيد دقة الموقّت من JavaScript عن عدة مللي ثانية.
- تختلف معدّلات التحديث في الأجهزة المختلفة.
تذكَّر مشكلة توقيت اللقطات المذكورة أعلاه: تحتاج إلى إطار متحرك مكتمل، تم الانتهاء منه باستخدام أي JavaScript أو تعديل DOM أو تنسيق أو رسم، وما إلى ذلك، ليكون جاهزًا قبل أن تحدث عملية إعادة تحميل الشاشة التالية. يمكن أن يؤدي انخفاض دقة الموقّت إلى صعوبة إكمال عرض إطارات الرسوم المتحركة قبل تحديث الشاشة التالي، ولكنّ الاختلاف في معدّلات تحديث الشاشة يجعل ذلك مستحيلاً باستخدام موقّت ثابت. بغض النظر عن الفاصل الزمني للموقّت، ستخرج تدريجيًا من نافذة التوقيت لأحد اللقطات وستنتهي بحذف لقطة. سيحدث ذلك حتى إذا تم تشغيل الموقّت بدقة تصل إلى جزء من الثانية، وهو ما لن يحدث (كما اكتشف المطوّرون) - يختلف دقة الموقّت حسب ما إذا كان الجهاز يعمل بالبطارية أو متصلاً بالطاقة، ويمكن أن يتأثر بعلامات التبويب في الخلفية التي تستهلك الموارد، وما إلى ذلك. حتى إذا كان ذلك نادرًا (على سبيل المثال، كل 16 لقطة بسبب تأخّر جزء من الثانية)، ستلاحظ أنّه سيتم حذف عدة لقطات في الثانية. وسيكون عليك أيضًا إنشاء إطارات لا يتم عرضها أبدًا، ما يؤدي إلى إهدار الطاقة ووقت وحدة المعالجة المركزية الذي كان بإمكانك إنفاقه في تنفيذ إجراءات أخرى في تطبيقك.
تختلف معدّلات التحديث في الشاشات المختلفة: يكون معدّل التحديث 60 هرتز بشكل شائع، ولكنّ بعض الهواتف تستخدم معدّل تحديث 59 هرتز، وبعض أجهزة الكمبيوتر المحمول تنخفض إلى 50 هرتز في وضع الطاقة المنخفضة، وبعض شاشات الكمبيوتر المكتبي تستخدم معدّل تحديث 70 هرتز.
نميل إلى التركيز على عدد اللقطات في الثانية عند مناقشة أداء العرض، ولكن يمكن أن يكون التباين مشكلة أكبر. تلاحظ أعيننا التوقفات الصغيرة غير المنتظمة في الصور المتحركة التي يمكن أن ينتج عنها عرض صور متحركة بتوقيت غير مناسب.
يمكنك استخدام requestAnimationFrame
للحصول على إطارات متحركة مُعدّة بشكل صحيح. عند استخدام واجهة برمجة التطبيقات هذه، يعني ذلك أنّك تطلب من المتصفّح إطارًا للحركة. يتمّ استدعاء دالة الاستدعاء عندما يُنشئ المتصفّح إطارًا جديدًا قريبًا. ويحدث ذلك بغض النظر عن معدّل إعادة التحميل.
تتمتع requestAnimationFrame
أيضًا بخصائص رائعة أخرى:
- يتم إيقاف الصور المتحركة في علامات التبويب التي تعمل في الخلفية مؤقتًا، ما يحافظ على موارد النظام وعمر البطارية.
- إذا لم يتمكّن النظام من عرض الرسومات بمعدّل تحديث الشاشة، يمكنه إبطاء عرض الصور المتحركة وعرض طلب إعادة الاتصال بمعدّل أقل (مثلاً، 30 مرة في الثانية على شاشة بمعدّل تحديث 60 هرتز). وفي حين أنّ هذا الإجراء يقلّل معدّل عرض اللقطات إلى النصف، إلا أنّه يحافظ على اتساق الصور المتحركة، وكما ذكرنا أعلاه، فإنّ أعيننا تتأقلم مع التباين أكثر من معدّل عرض اللقطات. تبدو معدلات الإطارات الثابتة التي تبلغ 30 لقطة في الثانية أفضل من معدلات الإطارات التي تبلغ 60 لقطة في الثانية والتي تفوّت بعض اللقطات في الثانية.
تمّت مناقشة requestAnimationFrame
في كل مكان، لذا يمكنك الرجوع إلى مقالات مثل هذه المقالة من creative JS للحصول على مزيد من المعلومات حولها، ولكنّها خطوة أولى مهمة لإنشاء رسوم متحركة سلسة.
ميزانية الإطار
بما أنّنا نريد أن يكون إطار جديد جاهزًا عند كل عملية إعادة تحميل للشاشة، لا يتوفّر سوى الوقت بين عمليات إعادة التحميل لتنفيذ كل العمل لإنشاء إطار جديد. على شاشة بمعدل تكرار 60 هرتز، يعني ذلك أنّ لدينا حوالي 16 ملي ثانية لتشغيل كل JavaScript وتنفيذ التنسيق والتلوين وأيّ شيء آخر يجب أن يفعله المتصفّح لعرض اللقطة. وهذا يعني أنّه إذا استغرق تنفيذ JavaScript داخل دالة الاستدعاء requestAnimationFrame
أكثر من 16 ملي ثانية، لن يكون بإمكانك إنشاء لقطة في الوقت المناسب لمزامنة الفيديو.
إنّ 16 ملي ثانية ليست وقتًا طويلاً. لحسن الحظ، يمكن أن تساعدك "أدوات المطوّرين" في Chrome في تتبُّع ما إذا كنت تتجاوز الحدّ الأقصى المسموح به لعدد اللقطات في الثانية أثناء طلب استدعاء requestAnimationFrame.
عند فتح المخطط الزمني في "أدوات المطوّرين" وتسجيل هذه الصورة المتحركة أثناء عرضها، يتبيّن لنا بسرعة أنّنا تجاوزنا الميزانية المحدّدة عند إنشاء الصور المتحركة. في "المخطط الزمني"، انتقِل إلى "اللقطات" واطّلِع على ما يلي:
تستغرق عمليات الاستدعاء requestAnimationFrame (rAF) هذه أكثر من 200 ملي ثانية. هذه المدة طويلة جدًا لعرض لقطة كل 16 ملي ثانية. يؤدي فتح أحد عمليات الاستدعاء الطويلة لـ rAF إلى الكشف عن ما يحدث داخل التطبيق، وفي هذه الحالة، يتم عرض الكثير من التنسيقات.
يقدّم فيديو "بول" تفاصيل أكثر حول السبب المحدّد لإعادة التنسيق (يظهر الرمز scrollTop
) وكيفية تجنُّبه. ولكن النقطة هنا هي أنّه يمكنك التعمّق في طلب معاودة الاتصال والتحقيق في سبب استغراق الأمر وقتًا طويلاً.
لاحظ وقت عرض اللقطة الذي يبلغ 16 ملي ثانية. هذه المساحة الفارغة في الإطارات هي المساحة التي يمكنك فيها إجراء المزيد من العمل (أو السماح للمتصفّح بتنفيذ المهام التي يحتاجها في الخلفية). هذه المساحة الفارغة جيدة.
مصدر آخر للبطء
إنّ أكبر سبب للمشاكل عند محاولة تشغيل الرسوم المتحرّكة المستندة إلى JavaScript هو أنّه يمكن أن تعترض عناصر أخرى طلب الاستدعاء في rAF، وقد تمنع تنفيذه تمامًا. حتى إذا كان طلب الاستدعاء في rAF بسيطًا ويتم تنفيذه في بضع ملي ثوانٍ فقط، يمكن أن تبدأ أنشطة أخرى (مثل معالجة طلب XHR الذي وصل للتو، أو تشغيل معالجات أحداث الإدخال، أو تنفيذ تعديلات مجدوَلة على موقّت) فجأة وتعمل لأي فترة زمنية بدون التوقّف. على الأجهزة المتحرّكة، قد تستغرق معالجة هذه الأحداث أحيانًا مئات المللي ثانية، وخلال هذه الفترة، سيتوقف عرض الصورة المتحركة تمامًا. ونطلق على هذه المقاطع المتعلّقة بالرسوم المتحركة اسم الارتباك.
لا تتوفّر طريقة سحرية لتجنّب هذه المواقف، ولكن هناك بعض أفضل الممارسات المعمارية التي تساعدك في تحقيق النجاح:
- لا تُجري الكثير من المعالجة في معالجات الإدخال. إنّ استخدام الكثير من JavaScript أو محاولة إعادة ترتيب الصفحة بأكملها أثناء معالجة onscroll مثلاً هو سبب شائع جدًا للانقطاع المفاجئ.
- انقل أكبر قدر ممكن من عمليات المعالجة (أي أيّ عملية ستستغرق وقتًا طويلاً لإكمالها) إلى دالة الاستدعاء في واجهة رسومات متحركة (rAF) أو Web Workers.
- إذا كنت تُجري عملية في دالة الاستدعاء rAF، حاوِل تقسيمها إلى أجزاء صغيرة حتى تتم معالجة جزء صغير فقط من كل إطار أو تأخيرها إلى أن تنتهي عملية إنشاء صورة متحركة مهمة. بهذه الطريقة، يمكنك مواصلة تنفيذ دوال استدعاء rAF قصيرة وإنشاء صور متحركة بسلاسة.
للحصول على دليل تعليمي رائع يتناول كيفية دفع المعالجة إلى وظائف الاستدعاء requestAnimationFrame بدلاً من معالجات الإدخال، يمكنك الاطّلاع على مقالة "بول لويس" إنشاء صور متحركة أكثر كفاءة وسرعة باستخدام requestAnimationFrame.
الصور المتحركة في CSS
ما هو أفضل من JavaScript خفيفة الوزن في وظائف الاستدعاء لأحداث rAF وأحداثك؟ لا تتوفّر JavaScript.
ذكرنا سابقًا أنّه ما مِن حلّ سحري لتجنُّب مقاطعة عمليات استدعاء rAF، ولكن يمكنك استخدام الرسوم المتحركة في CSS لتجنُّب الحاجة إليها تمامًا. في متصفّح Chrome لأجهزة Android على وجه الخصوص (وتعمل المتصفّحات الأخرى على ميزات مشابهة)، تتمتع الصور المتحركة في CSS بخاصية مرغوب فيها جدًا، وهي أنّ المتصفّح يمكنه غالبًا تشغيلها حتى إذا كان JavaScript قيد التشغيل.
هناك عبارة ضمنية في القسم أعلاه عن الارتباك: يمكن للمتصفّحات تنفيذ إجراء واحد فقط في كل مرة. هذا ليس صحيحًا تمامًا، ولكنّه افتراض عملي جيد: في أي وقت، يمكن للمتصفّح تشغيل JavaScript أو تنفيذ التخطيط أو الرسم، ولكنّه يمكن تنفيذ إجراء واحد فقط في المرة الواحدة. ويمكن التحقّق من ذلك في عرض "المخطط الزمني" ضمن "أدوات المطوّر". من استثناءات هذه القاعدة هي الرسوم المتحرّكة التي تستخدم لغة CSS على متصفّح Chrome لأجهزة Android (وقريبًا على متصفّح Chrome للكمبيوتر المكتبي، ولكن ليس بعد).
عند الإمكان، يمكنك استخدام رسوم متحركة من CSS لتسهيل استخدام تطبيقك والسماح بتشغيل الرسومات المتحركة بسلاسة، حتى أثناء تشغيل JavaScript.
// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
rAF = window.requestAnimationFrame;
var degrees = 0;
function update(timestamp) {
document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
console.log('updated to degrees ' + degrees);
degrees = degrees + 1;
rAF(update);
}
rAF(update);
في حال النقر على الزر، يتم تنفيذ JavaScript لمدة 180 ملي ثانية، ما يؤدي إلى حدوث تقطُّع. ولكن إذا استخدمنا الرسومات المتحركة في CSS بدلاً من ذلك، لن يحدث الارتباك.
(يُرجى العِلم أنّه في وقت كتابة هذه المقالة، لا يمكن عرض الرسوم المتحركة باستخدام CSS بسلاسة إلا على متصفّح Chrome لأجهزة Android، وليس على متصفّح Chrome للكمبيوتر المكتبي).
/* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
#foo {
+animation-duration: 3s;
+animation-timing-function: linear;
+animation-animation-iteration-count: infinite;
+animation-animation-name: rotate;
}
@+keyframes: rotate; {
from {
+transform: rotate(0deg);
}
to {
+transform: rotate(360deg);
}
}
لمزيد من المعلومات حول استخدام رسوم CSS المتحركة، اطّلِع على مقالات مثل هذه المقالة على MDN.
الخاتمة
في ما يلي ملخص لهذه التغييرات:
- عند إنشاء الصور المتحركة، من المهم إنتاج إطارات لكل عملية إعادة تحميل للشاشة. تترك الرسوم المتحركة التي تتضمّن ميزة "مزامنة الإطارات" تأثيرًا إيجابيًا كبيرًا على تجربة استخدام التطبيق.
- إنّ أفضل طريقة للحصول على حركة متسلسلة في Chrome والمتصفّحات الحديثة الأخرى هي استخدام ميزة CSS animation. عندما تحتاج إلى مرونة أكبر مما يوفّره ملف CSS المتحرّك، فإنّ أفضل أسلوب هو استخدام الصور المتحركة المستندة إلى requestAnimationFrame.
- للحفاظ على أداء الرسومات المتحركة في إطارات الاستبدال السريع للصور (rAF) بشكل جيد، تأكَّد من أنّ معالجات الأحداث الأخرى لا تعترض تنفيذ طلبات الاستدعاء في إطارات الاستبدال السريع للصور، واحرص على أن تكون طلبات الاستدعاء في إطارات الاستبدال السريع للصور قصيرة (<15 ملي ثانية).
أخيرًا، لا تنطبق الرسوم المتحركة التي تستخدم vsync فقط على الرسوم المتحركة البسيطة لواجهة المستخدم، بل تنطبق أيضًا على الرسوم المتحركة Canvas2D وWebGL، وحتى الانتقال إلى الأسفل أو الأعلى في الصفحات الثابتة. في المقالة التالية من هذه السلسلة، سنتناول بالتفصيل أداء الانتقال للأعلى أو للأسفل مع أخذ هذه المفاهيم في الاعتبار.
مع أطيب التحيّات،