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

بسیاری از سایت ها و برنامه ها دارای اسکریپت های زیادی برای اجرا هستند. جاوا اسکریپت شما اغلب باید در اسرع وقت اجرا شود، اما در عین حال نمی‌خواهید مانعی برای کاربر شود. اگر زمانی که کاربر در حال پیمایش صفحه است، داده‌های تحلیلی را ارسال می‌کنید، یا عناصری را به 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.body.appendChild(documentFragment);
    documentFragment = null;
}

همه چیز خوب است، اکنون هنگام اضافه کردن موارد به DOM، jank بسیار کمتری خواهیم دید. عالی!

سوالات متداول

  • پلی فیل وجود دارد؟ متأسفانه نه، اما اگر می‌خواهید یک تغییر مسیر شفاف به setTimeout داشته باشید، یک شیم وجود دارد . دلیل وجود این API این است که شکاف بسیار واقعی را در پلتفرم وب ایجاد می کند. استنباط عدم فعالیت دشوار است، اما هیچ API جاوا اسکریپتی برای تعیین میزان زمان آزاد در انتهای فریم وجود ندارد، بنابراین در بهترین حالت باید حدس بزنید. APIهایی مانند setTimeout ، setInterval ، یا setImmediate می‌توانند برای زمان‌بندی کار استفاده شوند، اما زمان‌بندی آن‌ها برای جلوگیری از تعامل کاربر مانند requestIdleCallback نیست.
  • اگر مهلت را تجاوز کنم چه اتفاقی می افتد؟ اگر timeRemaining() صفر را برمی گرداند، اما شما ترجیح می دهید برای مدت طولانی تری اجرا شود، می توانید بدون ترس از اینکه مرورگر کار شما را متوقف کند، این کار را انجام دهید. با این حال، مرورگر به شما مهلت می دهد تا سعی کنید تجربه ای روان را برای کاربران خود تضمین کنید، بنابراین، مگر اینکه دلیل بسیار خوبی وجود داشته باشد، همیشه باید به ضرب الاجل پایبند باشید.
  • آیا حداکثر مقداری وجود دارد که timeRemaining() برگرداند؟ بله، در حال حاضر 50 میلی‌ثانیه است. هنگام تلاش برای حفظ یک برنامه پاسخگو، تمام پاسخ ها به تعاملات کاربر باید کمتر از 100 میلی ثانیه نگه داشته شوند. در صورت تعامل کاربر، پنجره 50 میلی‌ثانیه باید در بیشتر موارد، امکان تکمیل تماس بی‌حرکتی را فراهم کند و مرورگر بتواند به تعاملات کاربر پاسخ دهد. ممکن است چندین تماس بیکار برنامه ریزی شده پشت سر هم دریافت کنید (اگر مرورگر تشخیص دهد که زمان کافی برای اجرای آنها وجود دارد).
  • آیا هیچ نوع کاری وجود دارد که من نباید در requestIdleCallback انجام دهم؟ در حالت ایده‌آل، کاری که انجام می‌دهید باید در تکه‌های کوچک (Microtask) باشد که ویژگی‌های نسبتاً قابل پیش‌بینی دارند. به عنوان مثال، تغییر DOM به طور خاص زمان اجرای غیرقابل پیش بینی خواهد داشت، زیرا محاسبات سبک، چیدمان، نقاشی و ترکیب را آغاز می کند. به این ترتیب، شما باید فقط همانطور که در بالا پیشنهاد شد، تغییرات DOM را در یک requestAnimationFrame انجام دهید. یکی دیگر از مواردی که باید مراقب آن بود، حل کردن (یا رد کردن) Promises است، زیرا تماس‌های برگشتی بلافاصله پس از پایان تماس بی‌کار اجرا می‌شوند، حتی اگر زمان بیشتری باقی نمانده باشد.
  • آیا من همیشه یک requestIdleCallback در پایان یک فریم دریافت خواهم کرد؟ نه همیشه نه هر زمان که در پایان یک فریم، یا در دوره‌هایی که کاربر غیرفعال است، زمان آزاد وجود داشته باشد، مرورگر پاسخ تماس را برنامه‌ریزی می‌کند. شما نباید انتظار داشته باشید که تماس برگشتی در هر فریم فراخوانی شود، و اگر نیاز دارید که در یک بازه زمانی مشخص اجرا شود، باید از تایم اوت استفاده کنید.
  • آیا می توانم چندین درخواست پاسخگوی requestIdleCallback داشته باشم؟ بله، شما می‌توانید، همانطور که می‌توانید چندین درخواست requestAnimationFrame داشته باشید. با این حال، شایان ذکر است که اگر اولین تماس شما از زمان باقیمانده در طول تماس خود استفاده کند، دیگر زمانی برای تماس های دیگر باقی نخواهد ماند. پس از آن، دیگر تماس‌های برگشتی باید منتظر بمانند تا مرورگر در حالت بی‌کار بعدی قرار گیرد تا بتوان آنها را اجرا کرد. بسته به کاری که می‌خواهید انجام دهید، ممکن است بهتر باشد یک تماس بی‌حرکت داشته باشید و کار را در آنجا تقسیم کنید. از طرف دیگر، می‌توانید از زمان‌بندی استفاده کنید تا اطمینان حاصل کنید که هیچ تماسی برای زمان کم نمی‌شود.
  • چه اتفاقی می‌افتد اگر یک تماس غیرفعال جدید در داخل دیگری تنظیم کنم؟ برنامه‌ریزی شده است که تماس بی‌کار جدید در اسرع وقت اجرا شود و از فریم بعدی (به جای فریم فعلی) شروع شود.

بیکار!

requestIdleCallback یک راه عالی برای اطمینان از اینکه می توانید کد خود را اجرا کنید، اما بدون اینکه مانعی برای کاربر شود، است. استفاده از آن ساده و بسیار انعطاف پذیر است. با این حال، هنوز روزهای اولیه است، و مشخصات به طور کامل حل نشده است، بنابراین هر گونه بازخوردی که داشته باشید استقبال می شود.

آن را در Chrome Canary بررسی کنید، آن را برای پروژه‌های خود مرور کنید و به ما اطلاع دهید که چگونه پیش می‌روید!