جدولة JavaScript أفضل باستخدام isInputPending()

واجهة برمجة تطبيقات JavaScript جديدة قد تساعدك في تجنُّب المفاضلة بين أداء التحميل وسرعة استجابة الإدخال

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

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

للتخلص من الحاجة إلى إجراء هذا التوازن، اقترح Facebook واجهة برمجة التطبيقات isInputPending() ونفّذها في Chromium لتحسين الاستجابة بدون الاستسلام. استنادًا إلى الملاحظات الواردة من مرحلة التجربة والتقييم، أجرينا عددًا من التعديلات على واجهة برمجة التطبيقات، ويسعدنا الإعلان عن أنّ واجهة برمجة التطبيقات يتم تضمينها الآن تلقائيًا في الإصدار 87 من Chromium.

توافُق المتصفح

Browser Support

  • Chrome: 87.
  • Edge: 87.
  • Firefox: not supported.
  • Safari: not supported.

Source

isInputPending() يتم تضمينها في المتصفّحات المستندة إلى Chromium بدءًا من الإصدار 87. لم يُظهر أي متصفّح آخر نية لطرح واجهة برمجة التطبيقات.

الخلفية

يتم تنفيذ معظم الأعمال في منظومة JS المتكاملة الحالية على سلسلة واحدة: السلسلة الرئيسية. يقدّم هذا الإجراء نموذج تنفيذ قويًا للمطوّرين، ولكن يمكن أن تتأثر تجربة المستخدم (سرعة الاستجابة على وجه الخصوص) بشكل كبير إذا تم تنفيذ النص البرمجي لفترة طويلة. إذا كانت الصفحة تُجري الكثير من الأعمال أثناء بدء حدث إدخال، على سبيل المثال، لن تعالج الصفحة حدث إدخال النقرة إلا بعد اكتمال هذا العمل.

تتمثل أفضل الممارسات الحالية في التعامل مع هذه المشكلة من خلال تقسيم برمجة ‎JavaScript إلى وحدات أصغر. أثناء تحميل الصفحة، يمكن للصفحة تنفيذ برمجة برمجة JavaScript، ثم تسليم التحكّم وإعادته إلى المتصفّح. يمكن للمتصفّح بعد ذلك التحقّق من قائمة انتظار أحداث الإدخال لمعرفة ما إذا كان هناك أيّ حدث يحتاج إلى إبلاغ الصفحة به. بعد ذلك، يمكن للمتصفّح العودة إلى تشغيل ملفّات برمجيّة JavaScript عند إضافتها. يساعد ذلك، ولكن يمكن أن يؤدي إلى مشاكل أخرى.

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

رسم بياني يوضّح أنّه عند تنفيذ مهام JavaScript طويلة، يكون لدى المتصفّح وقت أقل لإرسال الأحداث.

في Facebook، أردنا معرفة كيف ستكون الأمور إذا توصّلنا إلى أسلوب loading جديد يزيل هذا الخيار الصعب. لذلك، تواصلنا مع فريق Chrome بشأن هذا الأمر، وطرحنا الاقتراح التالي: isInputPending(). واجهة برمجة التطبيقات isInputPending() هي أوّل واجهة تستخدم مفهوم المقاطعات لإدخالات المستخدمين على الويب، وتسمح لواجهة JavaScript بالتحقّق من الإدخالات بدون التسليم للمتصفّح.

مخطّط بياني يوضّح أنّ دالة isInputPending() تسمح لـ JavaScript بالتحقّق مما إذا كانت هناك إدخالات من المستخدم في انتظار المراجعة، بدون إعادة التنفيذ بالكامل إلى المتصفّح

ونظرًا لاهتمام المطوّرين بواجهة برمجة التطبيقات، عقدنا شراكة مع زملائنا في Chrome لإدراج الميزة في Chromium وطرحها. وبمساعدة مهندسي Chrome، تم طرح الإصلاحات بعد مرحلة التجربة والتقييم (وهي طريقة يتّبعها Chrome لاختبار التغييرات والحصول على ملاحظات من المطوّرين قبل طرح واجهة برمجة التطبيقات بالكامل).

لقد أخذنا الآن الملاحظات الواردة من التجربة التجريبية للإصدار الأول ومن الأعضاء الآخرين في مجموعة عمل أداء الويب في W3C، ونفّذنا التغييرات على واجهة برمجة التطبيقات.

مثال: جدولة المُربِح

لنفترض أنّ لديك الكثير من الأعمال التي تؤدي إلى حظر العرض لتحميل صفحتك، مثل إنشاء ترميز من المكوّنات أو استبعاد الأرقام الأولية أو مجرد رسم مؤشر تحميل رائع. ويتم تقسيم كلّ منها إلى عنصر عمل منفصل. باستخدام نمط المخطِّط، لنوضِّح كيف يمكننا معالجة عملنا في دالة processWorkQueue() افتراضية:

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

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

هذا جيد، ولكن هل يمكننا تحسين الأداء؟ بكل تأكيد!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

من خلال تقديم طلب إلى navigator.scheduling.isInputPending()، يمكننا الردّ على الإدخال بشكل أسرع مع ضمان تنفيذ عملية حظر الشاشة بدون انقطاع. إذا لم نكن مهتمين بمعالجة أي ملف بخلاف الإدخال (مثل الرسم) إلى أن يكتمل العمل، يمكننا بسهولة زيادة طول QUANTUM أيضًا.

لا يتم تلقائيًا عرض الأحداث "المستمرة" من isInputPending(). وتشمل هذه القنوات mousemove وpointermove وغيرها. إذا كنت مهتمًا بتحقيق الربح من هذه القنوات أيضًا، ما مِن مشكلة. من خلال تقديم عنصر إلى isInputPending() مع تحديد includeContinuous على true، يمكننا المتابعة:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

هذا كل شيء! تعمل إطارات العمل، مثل React، على توفير isInputPending() في مكتبات جدولة أساسية باستخدام منطق مشابه. نأمل أن يؤدي ذلك إلى تمكّن المطوّرين الذين يستخدِمون هذه الأطر من الاستفادة من isInputPending() من وراء الكواليس بدون إجراء عمليات إعادة كتابة كبيرة.

التراجع ليس سيئًا دائمًا

تجدر الإشارة إلى أنّ تحقيق عائد أقل ليس الحل المناسب لكل حالة استخدام. هناك العديد من الأسباب التي تدفع إلى إعادة التحكّم إلى المتصفّح بخلاف معالجة أحداث الإدخال، مثل تنفيذ العرض وتنفيذ نصوص برمجية أخرى في الصفحة.

هناك حالات يتعذّر فيها على المتصفّح تحديد مصدر أحداث الإدخال التي لم تكتمل بعد بشكل صحيح. وعلى وجه الخصوص، قد يؤدي ضبط مقاطع وأقنعة معقّدة لإطارات div في صفحات ويب من مصادر مختلفة إلى الإبلاغ عن نتائج خاطئة (أي قد يعرض isInputPending() بشكل غير متوقّع قيمة "خطأ" عند استهداف هذه الإطارات). تأكّد من أنّك تُجري عمليات التقديم/الإرجاع بشكل كافٍ إذا كان موقعك الإلكتروني يتطلّب تفاعلات مع إطارات فرعية منمّقة.

انتبه أيضًا إلى الصفحات الأخرى التي تشارك حلقة أحداث. على منصات مثل Chrome لنظام التشغيل Android، من الشائع أن تشارك مصادر متعددة حلقة حدث. لن تعرض isInputPending() أبدًا القيمة true إذا تم إرسال الإدخال إلى إطار من مصدر مختلف، وبالتالي قد تتداخل الصفحات التي تعمل في الخلفية مع سرعة استجابة الصفحات التي تعمل في المقدّمة. قد تحتاج إلى تقليل معدّل إرسال البيانات أو تأجيله أو إيقافه بوتيرة أكبر عند تنفيذ مهام في الخلفية باستخدام واجهة برمجة التطبيقات Page Visibility API.

ننصحك باستخدام isInputPending() بحرص. إذا لم يكن هناك عمل يتعلّق بحظر المستخدمين، يمكنك مساعدة المستخدمين الآخرين في حلقة الأحداث من خلال الاستجابة لطلباتهم بشكلٍ متكرّر. قد تكون المهام الطويلة ضارّة.

الملاحظات

  • يُرجى إرسال ملاحظاتك حول المواصفات في مستودع is-input-pending.
  • يمكنك التواصل مع ‎@acomminos (أحد مؤلفي المواصفات) على Twitter.

الخاتمة

يسرّنا إطلاق isInputPending()، ويسرّنا أيضًا أنّه يمكن للمطوّرين بدأ استخدامه اليوم. هذه هي المرة الأولى التي تُنشئ فيها Facebook واجهة برمجة تطبيقات ويب جديدة وتنقلها من مرحلة حضانة الأفكار إلى اقتراح المعايير ثم إلى مرحلة الطرح في المتصفّح. نريد أن نشكر كل من ساعدنا في الوصول إلى هذه النقطة، ونريد أن نشكر بشكل خاص كل من ساعدنا في Chrome على تطوير هذه الفكرة وطرحها.

الصورة الرئيسية تقدّمها Will H McMahan على Unsplash.