إزالة البيانات غير الضرورية لتحسين أداء العرض

Tom Wiltzius
Tom Wiltzius

مقدمة

من المهم أن يكون تطبيق الويب سريع الاستجابة وسلسًا عند عرض الصور المتحركة والانتقالات وتأثيرات واجهة المستخدم الصغيرة الأخرى. إنّ التأكّد من أنّ هذه التأثيرات خالية من أي عيوب قد يُحدث فرقًا بين تجربة استخدام "أصلية" وتجربة استخدام غير سلسة.

هذه هي المقالة الأولى في سلسلة من المقالات التي تتناول تحسين أداء العرض في المتصفّح. في البداية، سنتناول سبب صعوبة إنشاء صور متحركة سلسة وما يجب فعله لتحقيق ذلك، بالإضافة إلى بعض أفضل الممارسات السهلة. تم تقديم العديد من هذه الأفكار في الأصل في "Jank Busters"، وهي محاضرة ألقيتها أنا و"نات دوكا" في مؤتمر Google I/O (فيديو) هذا العام.

نقدّم لك ميزة "المزامنة العمودية"

قد يكون لاعبو ألعاب الكمبيوتر على دراية بهذا المصطلح، ولكنّه غير شائع على الويب: ما هي المزامنة العمودية؟

لنفترض أنّ شاشة هاتفك يتم تحديثها على فترات منتظمة، عادةً (ولكن ليس دائمًا) حوالي 60 مرة في الثانية. تشير المزامنة العمودية (V-sync) إلى ممارسة إنشاء لقطات جديدة فقط بين عمليات إعادة تحميل الشاشة. يمكنك التفكير في هذا مثل حالة السباق بين العملية التي تكتب البيانات في المخزن المؤقت للشاشة ونظام التشغيل الذي يقرأ تلك البيانات لوضعها على الشاشة. نريد أن يتغيّر محتوى الإطارات المخزّنة مؤقتًا بين عمليات إعادة التحميل هذه، وليس أثناءها، وإلا ستعرض الشاشة نصف إطار ونصف إطار آخر، ما يؤدي إلى "تقطُّع".

للحصول على صورة متحركة سلسة، يجب أن يكون هناك إطار جديد جاهز في كل مرة يتم فيها تحديث الشاشة. وينتج عن ذلك تأثيران مهمّان: توقيت عرض اللقطة (أي الوقت الذي يجب أن تكون فيه اللقطة جاهزة) وميزانية اللقطة (أي الوقت الذي يستغرقه المتصفّح لإنشاء لقطة). لديك فقط الوقت بين تحديثات الشاشة لإكمال إطار (~ 16 مللي ثانية على شاشة 60 هرتز)، وتريد البدء في إنتاج الإطار التالي بمجرد وضع آخر إطار على الشاشة.

التوقيت هو كل شيء: requestAnimationFrame

يستخدم العديد من مطوّري تطبيقات الويب setInterval أو setTimeout كل 16 ملي ثانية لإنشاء صور متحركة. وهذا يمثّل مشكلة لعدة أسباب (وسنناقش المزيد بعد قليل)، ولكن ما يثير القلق بشكل خاص هو:

  • دقة الموقّت من JavaScript هي فقط في حدود عدة أجزاء من الثانية
  • تختلف معدّلات التحديث باختلاف الأجهزة

تذكَّر مشكلة وقت عرض اللقطة المذكورة أعلاه: يجب أن تكون لقطة الحركة المكتملة، والتي تم الانتهاء منها باستخدام أي JavaScript ومعالجة DOM والتنسيق وعرض محتوى الصفحة وما إلى ذلك، جاهزة قبل حدوث إعادة تحميل الشاشة التالية. قد يؤدي انخفاض دقة الموقّت إلى صعوبة إكمال إطارات الصور المتحركة قبل إعادة تحميل الشاشة التالية، ولكنّ الاختلاف في معدّلات إعادة تحميل الشاشة يجعل ذلك مستحيلاً باستخدام موقّت ثابت. بغض النظر عن الفاصل الزمني للموقّت، ستخرج ببطء من نافذة التوقيت لإطار ما وينتهي بك الأمر إلى إسقاط إطار. سيحدث ذلك حتى إذا تم تشغيل الموقّت بدقة بالمللي ثانية، وهو ما لن يحدث (كما اكتشف المطوّرون)، لأنّ دقة الموقّت تختلف حسب ما إذا كان الجهاز يعمل بالبطارية أو متصلاً بالكهرباء، ويمكن أن تتأثر بعلامات التبويب في الخلفية التي تستهلك الموارد، وما إلى ذلك. حتى إذا كان هذا نادرًا (مثلاً، كل 16 لقطة لأنّك تأخرت بمقدار مللي ثانية)، ستلاحظ أنّك ستفقد عدة لقطات في الثانية. ستعمل أيضًا على إنشاء إطارات لا يتم عرضها أبدًا، ما يؤدي إلى إهدار الطاقة ووقت وحدة المعالجة المركزية الذي يمكنك إنفاقه في القيام بأشياء أخرى في تطبيقك.

تختلف معدّلات التحديث باختلاف الشاشات: يُعدّ معدّل 60 هرتز شائعًا، ولكن بعض الهواتف تستخدم معدّل 59 هرتز، وبعض أجهزة الكمبيوتر المحمولة تنخفض إلى 50 هرتز في وضع توفير الطاقة، وبعض شاشات أجهزة الكمبيوتر المكتبي تستخدم معدّل 70 هرتز.

نركّز عادةً على عدد اللقطات في الثانية (FPS) عند مناقشة أداء العرض، ولكن قد يكون التباين مشكلة أكبر. تلاحظ أعيننا العوائق الصغيرة غير المنتظمة في الرسوم المتحركة التي يمكن أن تنتجها الرسوم المتحركة ذات التوقيت السيئ.

يمكنك الحصول على إطارات صور متحركة مضبوطة الوقت باستخدام 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 الذي تم إدخاله للتو، أو تشغيل معالِجات أحداث الإدخال، أو تشغيل التحديثات المجدولة على موقّت) فجأة وتعمل لأي فترة زمنية بدون توقف. على الأجهزة الجوّالة، قد تستغرق معالجة هذه الأحداث أحيانًا مئات الملّي ثانية، وخلال هذه الفترة، ستتوقف الرسوم المتحركة تمامًا. ونطلق على هذه المشاكل في الرسوم المتحركة اسم التقطّع.

ما مِن حلّ سحري لتجنُّب هذه المواقف، ولكن هناك بعض أفضل الممارسات المتعلّقة بالبنية التي تساعدك في تحقيق النجاح:

  • لا تُجرِ الكثير من عمليات المعالجة في معالِجات الإدخال. إنّ استخدام الكثير من لغة JS أو محاولة إعادة ترتيب الصفحة بأكملها أثناء استخدام معالج onscroll مثلاً هو سبب شائع جدًا لحدوث بطء شديد في الأداء.
  • ادفع أكبر قدر ممكن من المعالجة (أي أي شيء سيستغرق وقتًا طويلاً لتشغيله) إلى دالة الاستدعاء rAF أو Web Workers قدر الإمكان.
  • إذا دفعت العمل إلى استدعاء rAF، فحاول تقسيمه بحيث تقوم بمعالجة القليل فقط من كل إطار أو تأخيره حتى بعد انتهاء رسم متحرك مهم - وبهذه الطريقة يمكنك الاستمرار في تشغيل استدعاءات rAF القصيرة والتحريك بسلاسة.

للحصول على برنامج تعليمي رائع يتناول كيفية دفع المعالجة إلى عمليات استدعاء requestAnimationFrame بدلاً من معالِجات الإدخال، يمكنك الاطّلاع على مقالة بول لويس Leaner, Meaner, Faster Animations with requestAnimationFrame.

صور CSS المتحركة

ما هو أفضل من استخدام JavaScript خفيف الوزن في عمليات الاستدعاء للأحداث وrAF؟ لا يتم استخدام JavaScript.

ذكرنا سابقًا أنّه ما مِن حل سحري لتجنُّب مقاطعة عمليات استدعاء rAF، ولكن يمكنك استخدام تأثيرات CSS الحركية لتجنُّب الحاجة إليها تمامًا. في Chrome لنظام Android تحديدًا (وتعمل المتصفّحات الأخرى على ميزات مشابهة)، تتميّز حركات CSS بأنّ المتصفّح يمكنه تشغيلها في كثير من الأحيان حتى إذا كان JavaScript قيد التشغيل.

هناك عبارة ضمنية في القسم أعلاه حول البطء: يمكن للمتصفّحات تنفيذ إجراء واحد فقط في كل مرة. هذا ليس صحيحًا تمامًا، ولكنه افتراض عملي جيد: في أي وقت، يمكن للمتصفّح تشغيل JS أو تنفيذ التنسيق أو الرسم، ولكن يمكنه تنفيذ إجراء واحد فقط في كل مرة. يمكن التحقّق من ذلك في عرض "المخطط الزمني" ضمن "أدوات مطوّري البرامج". أحد الاستثناءات لهذه القاعدة هو صور 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 Animations، يمكنك الاطّلاع على مقالات مثل هذه المقالة على MDN.

الخاتمة

باختصار:

  1. عند إنشاء صور متحركة، من المهم إنتاج إطارات لكل عملية إعادة تحميل للشاشة. تُحدث الصور المتحركة التي تتم مزامنتها عموديًا تأثيرًا إيجابيًا كبيرًا في طريقة تفاعل المستخدم مع التطبيق.
  2. أفضل طريقة للحصول على صور متحركة متزامنة مع معدّل تحديث الشاشة في Chrome والمتصفّحات الحديثة الأخرى هي استخدام صور CSS المتحركة. عندما تحتاج إلى مرونة أكثر مما توفّره CSS animation، فإنّ أفضل أسلوب هو استخدام requestAnimationFrame-based animation.
  3. للحفاظ على سلامة وسلاسة الرسوم المتحركة التي تستخدم rAF، تأكَّد من أنّ معالِجات الأحداث الأخرى لا تعيق تنفيذ دالة الاستدعاء rAF، واحرص على أن تكون دوال الاستدعاء rAF قصيرة (أقل من 15 ملي ثانية).

أخيرًا، لا تنطبق الرسوم المتحركة المتزامنة مع معدّل التحديث على الرسوم المتحركة البسيطة لواجهة المستخدم فحسب، بل تنطبق أيضًا على الرسوم المتحركة في Canvas2D والرسوم المتحركة في WebGL وحتى التنقّل في الصفحات الثابتة. في المقالة التالية من هذه السلسلة، سنتناول أداء الانتقال مع وضع هذه المفاهيم في الاعتبار.

استمتِع بإنشاء الصور المتحركة.

المراجع