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