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