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

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

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

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

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

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

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

  • Chrome: 87
  • ‫Edge: 87
  • Firefox: غير متوافق
  • Safari: غير متوافق

المصدر

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

الخلفية

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

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

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

ملاحظات

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

الخاتمة

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

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