استخدام requestIdleCallback

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

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

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

لماذا عليّ استخدام requestIdleCallback؟

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

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

التحقّق من requestIdleCallback

لا يزال 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، وتجربته مع مشاريعك، وإعلامنا بمدى نجاحك.