با استفاده از requestIdleCallback

Paul Lewis

بسیاری از سایت ها و برنامه ها دارای اسکریپت های زیادی برای اجرا هستند. جاوا اسکریپت شما اغلب باید در اسرع وقت اجرا شود، اما در عین حال نمی‌خواهید مانعی برای کاربر شود. اگر زمانی که کاربر در حال پیمایش صفحه است، داده‌های تحلیلی را ارسال می‌کنید، یا عناصری را به DOM اضافه می‌کنید در حالی که روی دکمه ضربه می‌زنند، برنامه وب شما می‌تواند پاسخگو نباشد و در نتیجه تجربه کاربری ضعیفی داشته باشد.

استفاده از requestIdleCallback برای برنامه ریزی کارهای غیر ضروری.

خبر خوب این است که اکنون یک API وجود دارد که می تواند کمک کند: requestIdleCallback . همانطور که پذیرش requestAnimationFrame به ما این امکان را می‌دهد که انیمیشن‌ها را به‌درستی زمان‌بندی کنیم و شانس خود را برای رسیدن به سرعت ۶۰ فریم در ثانیه به حداکثر برسانیم، requestIdleCallback زمانی که زمان آزاد در انتهای یک فریم وجود دارد یا زمانی که کاربر غیرفعال است، کار را برنامه‌ریزی می‌کند. این به این معنی است که فرصتی برای انجام کار خود بدون ایجاد مزاحمت برای کاربر وجود دارد. از Chrome 47 در دسترس است، بنابراین می‌توانید امروز با استفاده از Chrome Canary به آن بچرخانید! این یک ویژگی آزمایشی است و مشخصات آن هنوز در حال تغییر است، بنابراین ممکن است اوضاع در آینده تغییر کند.

چرا باید از requestIdleCallback استفاده کنم؟

برنامه ریزی برای کارهای غیر ضروری خودتان بسیار دشوار است. تشخیص اینکه دقیقاً چقدر زمان فریم باقی می‌ماند غیرممکن است زیرا پس از اجرای callbacks requestAnimationFrame ، محاسبات سبک، طرح‌بندی، رنگ و سایر موارد داخلی مرورگر وجود دارد که باید اجرا شوند. یک راه حل خانگی نمی تواند هیچ یک از این موارد را توضیح دهد. برای اطمینان از اینکه کاربر به نحوی در حال تعامل نیست ، باید شنوندگان را به هر نوع رویداد تعاملی ( scroll ، touch ، click ) وصل کنید، حتی اگر برای عملکرد به آنها نیازی نداشته باشید، فقط برای اینکه کاملاً مطمئن شوید که کاربر در حال تعامل نیست. از سوی دیگر، مرورگر دقیقاً می‌داند چه مقدار زمان در انتهای فریم در دسترس است و آیا کاربر در حال تعامل است یا خیر، و بنابراین از طریق requestIdleCallback یک API به دست می‌آوریم که به ما امکان می‌دهد از هر زمان آزاد به کارآمدترین شکل ممکن استفاده کنیم.

بیایید کمی با جزئیات بیشتر به آن نگاه کنیم و ببینیم چگونه می توانیم از آن استفاده کنیم.

در حال بررسی درخواست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 است که تابع callback را به عنوان اولین پارامتر خود می گیرد:

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 است، اما تفاوت آن در این است که یک پارامتر دوم اختیاری را می گیرد: یک شی گزینه با ویژگی timeout . این مهلت زمانی، در صورت تنظیم، به مرورگر زمانی بر حسب میلی ثانیه می دهد که باید در آن بازخوانی را اجرا کند:

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

اگر پاسخ تماس شما به دلیل شلیک تایم اوت اجرا شود، متوجه دو چیز خواهید شد:

  • timeRemaining() صفر را برمی گرداند.
  • ویژگی didTimeout شیء deadline درست خواهد بود.

اگر می بینید که 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 Analytics اجتناب کنیم. ما آرایه ای از رویدادها را برای ارسال ایجاد می کنیم و درخواست می کنیم که آنها در مقطعی در آینده ارسال شوند:

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();
    }
}

در اینجا می توانید ببینید که من یک بازه زمانی 2 ثانیه ای تنظیم کرده ام، اما این مقدار به برنامه شما بستگی دارد. برای داده‌های تحلیلی، منطقی است که از مهلت زمانی استفاده شود تا اطمینان حاصل شود که داده‌ها در یک بازه زمانی معقول گزارش می‌شوند نه فقط در زمانی در آینده.

در نهایت باید تابعی را بنویسیم که 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 وجود نداشت، داده های تجزیه و تحلیل باید فورا ارسال شوند. با این حال، در یک برنامه تولیدی، احتمالاً بهتر است ارسال را با یک بازه زمانی به تعویق بیندازید تا مطمئن شوید که با هیچ گونه فعل و انفعالی در تضاد نیست و باعث jank نمی شود.

استفاده از requestIdleCallback برای ایجاد تغییرات DOM

موقعیت دیگری که در آن requestIdleCallback واقعاً می‌تواند به عملکرد کمک کند، زمانی است که باید تغییرات غیر ضروری DOM انجام دهید، مانند اضافه کردن موارد به انتهای یک لیست همیشه در حال رشد و بارگذاری تنبل. بیایید ببینیم که requestIdleCallback چگونه در یک فریم معمولی قرار می گیرد.

یک قاب معمولی

این امکان وجود دارد که مرورگر برای اجرای هر بازخوانی در یک فریم معین بیش از حد شلوغ باشد، بنابراین نباید انتظار داشته باشید که در پایان یک فریم زمان خالی برای انجام کارهای بیشتری وجود داشته باشد. این آن را با چیزی مانند setImmediate که در هر فریم اجرا می شود متفاوت می کند.

اگر فراخوانی در انتهای فریم اجرا شود ، برنامه‌ریزی می‌شود که پس از متعهد شدن فریم فعلی انجام شود، به این معنی که تغییرات سبک اعمال می‌شود، و مهمتر از آن، طرح‌بندی محاسبه می‌شود. اگر تغییرات DOM را در داخل فراخوان بیکار انجام دهیم، آن محاسبات طرح بندی باطل می شوند. اگر هر نوع صفحه بندی خوانده شده در فریم بعدی وجود داشته باشد، به عنوان مثال getBoundingClientRect ، clientWidth ، و غیره، مرورگر باید یک طرح بندی همزمان اجباری را انجام دهد که یک گلوگاه بالقوه عملکرد است.

یکی دیگر از دلایلی که باعث ایجاد تغییرات DOM در تماس بیکار نمی شود این است که تأثیر زمانی تغییر DOM غیرقابل پیش بینی است و به همین دلیل می توانیم به راحتی از مهلت تعیین شده مرورگر عبور کنیم.

بهترین روش این است که فقط تغییرات DOM را در داخل یک requestAnimationFrame انجام دهید، زیرا توسط مرورگر با در نظر گرفتن آن نوع کار برنامه ریزی شده است. این بدان معناست که کد ما باید از یک قطعه سند استفاده کند، که سپس می‌تواند در پاسخ requestAnimationFrame بعدی اضافه شود. اگر از کتابخانه VDOM استفاده می‌کنید، از requestIdleCallback برای ایجاد تغییرات استفاده می‌کنید، اما وصله‌های DOM را در callback بعدی 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