استخدام requestIdleCallback

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

استخدام requestIdleCallback لجدولة المهام غير الأساسية

والخبر السارّ هو أنّه تتوفّر الآن واجهة برمجة تطبيقات يمكنها المساعدة في: requestIdleCallback. بالطريقة نفسها التي سمحت لنا فيها تقنية requestAnimationFrame بجدولة الرسوم المتحرّكة بشكلٍ سليم وزيادة فرصنا في الوصول إلى 60 لقطة في الثانية إلى أقصى حد، سيحدّد requestIdleCallback وقتًا لتنفيذ العمل عندما يكون هناك وقت فارغ في نهاية اللقطة أو عندما يكون المستخدم غير نشط. وهذا يعني أنّ هناك فرصة لإجراء عملك بدون إعاقة المستخدم. تتوفّر هذه الميزة اعتبارًا من الإصدار 47 من Chrome، لذا يمكنك تجربتها اليوم باستخدام Chrome Canary. هذه ميزة تجريبية، ولا تزال المواصفات غير ثابتة، لذا قد تتغيّر الأمور في المستقبل.

لماذا يجب استخدام requestIdleCallback؟

من الصعب جدًا جدولة العمل غير الضروري بنفسك. من المستحيل معرفة الوقت المتبقي بالضبط لعرض اللقطة لأنّه بعد تنفيذ طلبات requestAnimationFrame التي تُعاد استدعاؤها، هناك عمليات حسابية للأسلوب والتنسيق والرسم وغيرها من العمليات الداخلية للمتصفّح التي يجب تنفيذها. لا يمكن لأي حلّ مُعدّ داخل المؤسسة أن يراعي أيًا من هذه العوامل. للتأكّد من أنّ المستخدم لا يتفاعل بطريقة معيّنة، عليك أيضًا إرفاق مستمعين بكلّ نوع من أحداث التفاعل (scroll وtouch وclick)، حتى إذا لم تكن بحاجة إليهم للوظائف، فقط للتأكّد تمامًا من أنّ المستخدم لا يتفاعل. من ناحية أخرى، يعرف المتصفّح بالضبط الوقت المتبقّي في نهاية اللقطة، وما إذا كان المستخدم يتفاعل، وبالتالي من خلال requestIdleCallback، نحصل على واجهة برمجة تطبيقات تتيح لنا الاستفادة من أي وقت فارغ بأكبر قدر ممكن من الكفاءة.

لنلقِ نظرة على ذلك بمزيد من التفصيل ونرى كيف يمكننا الاستفادة منه.

التحقّق من طلبIdleCallback

لا يزال requestIdleCallback في مراحله الأولى، لذا قبل استخدامه، عليك التحقّق من توفّره:

if ('requestIdleCallback' in window) {
    // Use requestIdleCallback to schedule work.
} else {
    // Do what you’d do today.
}

يمكنك أيضًا تعديل سلوكه، ما يتطلّب الرجوع إلى setTimeout:

window.requestIdleCallback =
    window.requestIdleCallback ||
    function (cb) {
    var start = Date.now();
    return setTimeout(function () {
        cb({
        didTimeout: false,
        timeRemaining: function () {
            return Math.max(0, 50 - (Date.now() - start));
        }
        });
    }, 1);
    }

window.cancelIdleCallback =
    window.cancelIdleCallback ||
    function (id) {
    clearTimeout(id);
    }

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

في الوقت الحالي، لنفترض أنّه متوفّر.

استخدام requestIdleCallback

يشبه استدعاء requestIdleCallback استدعاء requestAnimationFrame كثيرًا من حيث أنّه يأخذ دالة ردّ اتصال كمَعلمته الأولى:

requestIdleCallback(myNonEssentialWork);

عند استدعاء myNonEssentialWork، سيتم منحه عنصر deadline يحتوي على دالة تعرض رقمًا يشير إلى الوقت المتبقّي لعملك:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

يمكن استدعاء الدالة timeRemaining للحصول على أحدث قيمة. عندما يعرض timeRemaining() القيمة صفر، يمكنك جدولة requestIdleCallback آخر إذا كان لا يزال لديك المزيد من العمل:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

ضمان استدعاء الدالة

ماذا تفعل إذا كانت الأمور مشغولة جدًا؟ قد تشعر بالقلق من أنّه قد لا يتم الاتصال بك مرة أخرى. على الرغم من أنّ requestIdleCallback يشبه requestAnimationFrame، إلا أنّه يختلف أيضًا في أنّه يأخذ مَعلمة ثانية اختيارية: عنصر خيارات يتضمّن سمة مهلة. تمنح هذه المهلة للمتصفّح وقتًا بالملي ثانية يجب أن ينفذ خلاله طلب إعادة الاتصال:

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

إذا تم تنفيذ طلب الاستدعاء بسبب انتهاء مهلة الانتظار، ستلاحظ شيئين:

  • ستعرض الدالة timeRemaining() القيمة صفرًا.
  • ستكون السمة didTimeout لكائن deadline هي true.

إذا تبيّن لك أنّ القيمة didTimeout صحيحة، من المرجّح أن تريد فقط تنفيذ العمل وإنجازه:

function myNonEssentialWork (deadline) {

    // Use any remaining time, or, if timed out, just run through the tasks.
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
            tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

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

استخدام requestIdleCallback لإرسال بيانات الإحصاءات

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

var eventsToSend = [];

function onNavOpenClick () {

    // Animate the menu.
    menu.classList.add('open');

    // Store the event for later.
    eventsToSend.push(
    {
        category: 'button',
        action: 'click',
        label: 'nav',
        value: 'open'
    });

    schedulePendingEvents();
}

سنحتاج الآن إلى استخدام requestIdleCallback لمعالجة أي أحداث في انتظار المراجعة:

function schedulePendingEvents() {

    // Only schedule the rIC if one has not already been set.
    if (isRequestIdleCallbackScheduled)
    return;

    isRequestIdleCallbackScheduled = true;

    if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
    } else {
    processPendingAnalyticsEvents();
    }
}

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

أخيرًا، نحتاج إلى كتابة الدالة التي ستنفذها requestIdleCallback.

function processPendingAnalyticsEvents (deadline) {

    // Reset the boolean so future rICs can be set.
    isRequestIdleCallbackScheduled = false;

    // If there is no deadline, just run as long as necessary.
    // This will be the case if requestIdleCallback doesn’t exist.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
    }

    // Check if there are more events still to send.
    if (eventsToSend.length > 0)
    schedulePendingEvents();
}

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

استخدام requestIdleCallback لإجراء تغييرات على DOM

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

إطار نموذجي

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

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

هناك سبب آخر لعدم بدء تغييرات DOM في دالة الاستدعاء في حالة عدم النشاط، وهو أنّ التأثير الزمني لتغيير DOM غير متوقَّع، وبالتالي يمكننا بسهولة تجاوز الموعد النهائي الذي حدّده المتصفّح.

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

مع وضع ذلك في الاعتبار، لنلقِ نظرة على الرمز البرمجي:

function processPendingElements (deadline) {

    // If there is no deadline, just run as long as necessary.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    if (!documentFragment)
    documentFragment = document.createDocumentFragment();

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
    }

    // Check if there are more events still to send.
    if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

هنا أُنشئ العنصر وأستخدِم السمة textContent لملئه، ولكن من المرجّح أن تكون رمزية إنشاء العنصر أكثر تعقيدًا. بعد إنشاء العنصر، يتمّ استدعاء scheduleVisualUpdateIfNeeded، ما سيؤدّي إلى إعداد طلب استدعاء requestAnimationFrame واحد سيُلحق بدوره مقتطف المستند بالنصّ:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

في حال سارت الأمور على ما يرام، سنلاحظ الآن انخفاضًا كبيرًا في الأداء المتقطّع عند إلحاق العناصر بعنصر DOM. ممتازة

الأسئلة الشائعة

  • هل هناك polyfill؟ لا، ولكن تتوفّر خدعة إذا كنت تريد إعادة توجيه شفافة إلى setTimeout. تم توفير واجهة برمجة التطبيقات هذه لأنها تسدّ فجوة حقيقية في منصة الويب. من الصعب استنتاج عدم توفّر نشاط، ولكن لا تتوفّر واجهات برمجة تطبيقات JavaScript لتحديد مقدار الوقت المتاح في نهاية اللقطة، لذا عليك في أحسن الأحوال إجراء تخمينات. يمكن استخدام واجهات برمجة التطبيقات، مثل setTimeout أو setInterval أو setImmediate لجدولة العمل، ولكن لا يتم ضبط وقتها لتجنُّب تفاعل المستخدم بالطريقة التي يتم بها ضبط requestIdleCallback.
  • ماذا يحدث في حال تجاوزت الموعد النهائي؟ إذا كانت timeRemaining() تعرِض القيمة صفرًا، ولكنك اخترت تنفيذها لفترة أطول، يمكنك إجراء ذلك بدون خوف من أن يوقف المتصفّح عملك. ومع ذلك، يمنحك المتصفّح الموعد النهائي لمحاولة ضمان تجربة سلسة للمستخدمين، لذا عليك الالتزام بالموعد النهائي في كل الأوقات ما لم يكن لديك سبب وجيه جدًا.
  • هل هناك حدّ أقصى للقيمة التي سيعرضها timeRemaining()؟ نعم، المدة الحالية هي 50 ملي ثانية. عند محاولة الحفاظ على تطبيق سريع الاستجابة، يجب أن تكون جميع الردود على تفاعلات المستخدمين أقل من 100 ملي ثانية. إذا تفاعل المستخدم، من المفترض أن تسمح فترة الـ 50 ملي ثانية في معظم الحالات بإكمال طلب الاستدعاء في حالة عدم النشاط، وأن يستجيب المتصفّح لتفاعلات المستخدم. قد تتلقّى عدة عمليات استدعاء غير نشِطة مُجدوَلة بشكل متتالٍ (إذا رصد المتصفّح أنّ هناك وقتًا كافيًا لتنفيذها).
  • هل هناك أي نوع من العمل لا يجب أن أُجريه في requestIdleCallback؟ من الأفضل أن يكون العمل الذي تُجريه مجزّأً إلى أجزاء صغيرة (مهام صغيرة) لها خصائص يمكن توقّعها نسبيًا. على سبيل المثال، سيؤدي تغيير DOM على وجه الخصوص إلى أوقات تنفيذ غير متوقّعة، لأنّه سيؤدي إلى بدء عمليات حساب الأنماط والتنسيق والرسم والتركيب. وبناءً على ذلك، يجب إجراء تغييرات على DOM في دالة استدعاء requestAnimationFrame فقط كما هو مقترَح أعلاه. هناك أمر آخر يجب الحذر منه، وهو حلّ (أو رفض) الوعد، لأنّ وظائف الاستدعاء سيتم تنفيذها فورًا بعد انتهاء وظيفة الاستدعاء في حالة عدم النشاط، حتى إذا لم يكن هناك وقت متبقٍّ.
  • هل سأحصل دائمًا على requestIdleCallback في نهاية اللقطة؟ لا، ليس دائمًا. سيحدّد المتصفّح موعدًا لإعادة الاتصال في حال توفّر وقت فارغ في نهاية إطار أو في الفترات التي لا يكون فيها المستخدم نشطًا. لا يجب أن تتوقّع أن يتمّ استدعاء الدالة المرجعية لكلّ لقطة، وإذا كنت بحاجة إلى تشغيلها خلال إطار زمني معيّن، عليك استخدام مهلة الانتظار.
  • هل يمكنني الحصول على عدّة عمليات استدعاء requestIdleCallback؟ نعم، يمكنك ذلك، تمامًا كما يمكنك الحصول على مكالمات requestAnimationFrame متعددة. تجدر الإشارة إلى أنّه إذا استنفدت المكالمة المُعاد الاتصال بها للمرة الأولى الوقت المتبقّي أثناء إعادة الاتصال، لن يتبقّى وقت لأي مكالمات أخرى مُعاد الاتصال بها. وسيكون على طلبات الاستدعاء الأخرى الانتظار إلى أن يصبح المتصفّح غير نشِط في المرة التالية قبل أن يتم تشغيلها. استنادًا إلى العمل الذي تحاول تنفيذه، قد يكون من الأفضل الحصول على طلب إعادة اتصال واحد في حالة عدم النشاط وتقسيم العمل فيه. بدلاً من ذلك، يمكنك الاستفادة من المهلة لضمان عدم تأخُّر أيّ عمليات استدعاء.
  • ماذا يحدث إذا ضبطتُ طلب استدعاء جديدًا في وضع السكون داخل طلب آخر؟ سيتم جدولة تنفيذ طلب الاستدعاء الجديد في حالة عدم النشاط في أقرب وقت ممكن، بدءًا من الإطار التالي (بدلاً من الإطار الحالي).

في وضع الخمول.

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

يمكنك الاطّلاع على هذه الميزة في Chrome Canary وتجربتها في مشاريعك وإخبارنا برأيك.