بسیاری از سایت ها و برنامه ها دارای اسکریپت های زیادی برای اجرا هستند. جاوا اسکریپت شما اغلب باید در اسرع وقت اجرا شود، اما در عین حال نمیخواهید مانعی برای کاربر شود. اگر زمانی که کاربر در حال پیمایش صفحه است، دادههای تحلیلی را ارسال میکنید، یا عناصری را به DOM اضافه میکنید در حالی که روی دکمه ضربه میزنند، برنامه وب شما میتواند پاسخگو نباشد و در نتیجه تجربه کاربری ضعیفی داشته باشد.
خبر خوب این است که اکنون یک 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 بررسی کنید، آن را برای پروژههای خود مرور کنید و به ما اطلاع دهید که چگونه پیش میروید!