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

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

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

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

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

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

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

  • 87
  • 87
  • x
  • x

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

الخلفية

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

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

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

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

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

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

ونظرًا لوجود اهتمام بواجهة برمجة التطبيقات، تعاونّا مع زملائنا في 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() وراء الكواليس بدون الحاجة إلى إجراء عمليات إعادة صياغة كبيرة.

تحقيق الأهداف ليس سيئًا دائمًا

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

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

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

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

إضافة ملاحظات

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

الخلاصة

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

صورة رئيسية من تصوير ويل إتش ماك ماهان على قناة UnLaunch