เว็บไซต์และแอปจำนวนมากมีสคริปต์ที่ต้องใช้งานจำนวนมาก โดยปกติ JavaScript จะต้องเรียกใช้โดยเร็วที่สุด แต่ขณะเดียวกันก็คุณไม่ต้องการให้ขัดขวางการใช้งานของผู้ใช้ หากคุณส่งข้อมูลวิเคราะห์เมื่อผู้ใช้เลื่อนหน้าเว็บ หรือคุณเพิ่มองค์ประกอบลงใน DOM ขณะที่ผู้ใช้กำลังแตะปุ่ม เว็บแอปอาจไม่ตอบสนอง ซึ่งส่งผลให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ไม่ดี
ข่าวดีคือตอนนี้มี API ที่ช่วยrequestIdleCallback
ได้แล้ว ในลักษณะเดียวกับที่การใช้ requestAnimationFrame
ช่วยให้เราตั้งเวลาภาพเคลื่อนไหวได้อย่างถูกต้องและเพิ่มโอกาสที่จะได้รับอัตราเฟรมถึง 60 FPS ให้มากที่สุด requestIdleCallback
จะกำหนดเวลาการทำงานเมื่อมีเวลาว่างเมื่อสิ้นสุดเฟรมหรือเมื่อผู้ใช้ไม่ได้ใช้งาน ซึ่งหมายความว่าคุณจะมีโอกาสทำงานโดยไม่ขัดขวางการใช้งานของผู้ใช้ โดย Chrome 47 มีให้ใช้งานตั้งแต่ Chrome 47 เป็นต้นไป พร้อมให้คุณทดลองใช้แล้ววันนี้โดยใช้ Chrome Canary แต่เป็นฟีเจอร์ที่ทดสอบ และข้อกำหนดยังคงอยู่อย่างไม่หยุดหย่อน จึงอาจมีการเปลี่ยนแปลงในอนาคต
เหตุใดฉันจึงควรใช้ requestIdleCallback
���ารกำหนดเวลางานที่ไม่จำเป็นด้วยตัวเองนั้นทำได้ยากมาก คุณไม่สามารถบอกได้อย่างแน่ชัดว่าเวลาที่ใช้ในการแสดงผลเฟรมเหลือเท่าใดเนื่องจากหลังจากที่ requestAnimationFrame
เรียกใช้การติดต่อกลับจะมีการคำนวณรูปแบบ การจัดวาง การแสดงผล และภายในเบราว์เซอร์อื่นๆ ที่จำเป็นต้องเรียกใช้ โซลูชันแบบโฮมเมดไม่สามารถรองรับวิธีทั้ง 2 อย่างนี้ได้ ทั้งนี้เพื่อให้แน่ใจว่าผู้ใช้จะไม่ได้โต้ตอบในทางใดทางหนึ่ง คุณจึงต้องแนบ Listener ลงในเหตุการณ์การโต้ตอบทุกประเภท (scroll
, touch
, click
) ด้วย แม้จะไม่จำเป็นต้องใช้ฟังก์ชันดังกล่าวก็ตาม เพียงเพื่อให้มั่นใจว่าผู้ใช้ไม่ได้โต้ตอบ ในทางกลับกัน เบราว์เซอร์จะรู้แน่ชัดว่าเหลือเวลาเท่าใดในตอนท้ายของเฟรม และทราบว่าผู้ใช้มีการโต้ตอบหรือไม่ ดังนั้น requestIdleCallback
ทำให้เราได้ API ที่ช่วยให้เราใช้เวลาว่างต่างๆ ได้อย่างมีประสิทธิภาพมากที่สุด
มาดูรายละเอียดเพิ่มเติมสักเล็กน้อยและดูว่าเราสามารถใช้ประโยชน์จากสิ่งนี้ได้อย่างไร
กำลังตรวจสอบ 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
ไม่พร้อมใช้งาน การกะพริบด้วยวิธีนี้จึงไม่แย่ลงอีก เมื่อใช้ shim ควร requestIdleCallback
พร้อมใช้งาน ระบบจะเปลี่ยนเส้นทางการโทรของคุณเงียบๆ ซึ่งเป็นสิ่งที่เยี่ยมมาก
สำหรับตอนนี้ ให้สมมติว่ามีอยู่
การใช้ requestIdleCallback
การเรียก requestIdleCallback
คล้ายกับ requestAnimationFrame
มากตรงที่จะใช้ฟังก์ชันเรียกกลับเป็นพารามิเตอร์แรก ดังนี้
requestIdleCallback(myNonEssentialWork);
เมื่อเรียก myNonEssentialWork
ระบบจะได้รับออบเจ็กต์ deadline
ซึ่งมีฟังก์ชันซึ่งจะแสดงผลตัวเลขซึ่งระบุเวลาที่เหลือสำหรับงานของคุณ
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0)
doWorkIfNeeded();
}
คุณสามารถเรียกใช้ฟังก์ชัน timeRemaining
เพื่อรับค่าล่าสุด เมื่อ timeRemaining()
แสดงผลเป็น 0 คุณจะกำหนดเวลาได้อีกrequestIdleCallback
หากยังมีงานที่ต้องทำเพิ่มเติมอยู่
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
รับประกันการเรียกใช้ฟังก์ชัน
คุณจะทำอย่างไรหากมีงานล้นมือ คุณอาจกังวลว่าอาจไม่มีการติดต่อกลับ แม้ว่า requestIdleCallback
จะคล้ายๆ requestAnimationFrame
แต่ก็มีความแตกต่างอยู่ตรงที่จะใช้พารามิเตอร์ที่ 2 ที่ไม่บังคับ ซึ่งก็คือออบเจ็กต์ตัวเลือกที่มีพร็อพเพอร์ตี้ระยะหมดเวลา การหมดเวลานี้จะทำให้เบราว์เซอร์มีเวลาเป็นมิลลิวินาทีที่ต้องทำการติดต่อกลับ:
// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
หากมีการเรียกใช้โค้ดเรียกกลับเนื่องจากหมดเวลาเริ่มทำงาน คุณจะเห็น 2 สิ่งต่อไปนี้
timeRemaining()
จะแสดงผล 0- พร็อพเพอร์ตี้
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
ควรส่งข้อมูล Analytics ทันที อย่างไรก็ตาม ในแอปพลิเคชันเวอร์ชันที่ใช้งานจริง การชะลอการส่งโดยให้หมดเวลาอาจเป็นการที่ดีกว่า เพื่อให้แน่ใจว่าจะไม่ขัดแย้งกับการโต้ตอบใดๆ และก่อให้เกิดความยุ่งยาก
การใช้ requestIdleCallback เพื่อทำการเปลี่ยนแปลง DOM
อีกสถานการณ์หนึ่งที่ requestIdleCallback
จะช่วยเพิ่มประสิทธิภาพได้จริงคือเมื่อคุณมีการเปลี่ยนแปลง DOM ที่ไม่จำเป็น เช่น การเพิ่มรายการต่อท้ายรายการแบบ Lazy Loading ที่มีขนาดใหญ่ขึ้นเรื่อยๆ มาดูกันว่า requestIdleCallback
เหมาะกับเฟรมทั่วไปอย่างไร
อาจเป็นไปได้ว่าเบราว์เซอร์ไม่ว่างที่จะเรียกใช้ Callback ใดๆ ในเฟรมหนึ่งๆ ดังนั้นคุณไม่ควรคาดหวังว่าจะมีเวลาว่างใดๆ ในช่วงท้ายของเฟรมในการทำงานเพิ่มเติม ผลลัพธ์ที่ได้จึงแตกต่างจากอย่าง setImmediate
ที่แสดงต่อเฟรม
หากโค้ดเรียกกลับทำงานเมื่อสิ้นสุดเฟรม ระบบจะกำหนดเวลาให้โค้ดเรียกกลับทำงานหลังจากคอมมิตเฟรมปัจจุบันแล้ว ซึ่งหมายความว่า��ะมีการใช้การเปลี่ยนแปลงรูปแบบ และที่สำคัญคือมีการคำนวณเลย์เอาต์ หากเราทำการเปลี่ยนแปลง DOM ภายในโค้ดเรียกกลับที่ไม่มีการใช้งาน การคำนวณเลย์เอาต์เหล่านั้นจะใช้ไม่ได้ หากมีการอ่านเลย์เอาต์ประเภทใดก็ตามในเฟรมถัดไป เช่น getBoundingClientRect
, clientWidth
เป็นต้น เบราว์เซอร์จะต้องใช้ Forced Synchronous Layout ซึ่งเป็นจุดคอขวดด้านประสิทธิภาพที่เป็นไปได้
อีกเหตุผลหนึ่งที่ไม่ทำให้เกิดการเปลี่ยนแปลง 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 ยอดเยี่ยม
คำถามที่พบบ่อย
- มีโพลีฟิลไหม
ไม่จริง แต่มี shim ด้วย หากคุณต้องการเปลี่ยนเส้นทางไปยัง
setTimeout
อย่างโปร่งใส เหตุผลที่เกิด API นี้ขึ้นก็เพราะเป็นอุดช่องว่างจริงๆ ในแพลตฟอร์มบนเว็บ การคาดการณ์ถึงการขาดกิจกรรมนั้นทำได้ยาก แต่ไม่มี JavaScript API ที่จะช่วยกำหนดเวลาว่างในตอนท้ายของเฟรม ดังนั้นคุณต้องคาดเดาให้ดีที่สุด คุณใช้ API เช่นsetTimeout
,setInterval
หรือsetImmediate
เพื่อกำหนดเวลางานได้ แต่จะไม่มีการกำหนดเวลาเพื่อหลีกเลี่ยงการโต้ตอบของผู้ใช้ในลักษณะเดียวกับrequestIdleCallback
- จะเกิดอะไรขึ้นหากฉันทำงานเกินกำหนดเวลา
หาก
timeRemaining()
แสดงผลเป็น 0 แต่คุณเลือกให้แสดงเป็นเวลานานกว่านี้ คุณก็สามารถทำได้โดยไม่ต้องกลัวว่าเบราว์เซอร์จะหยุดการทำงาน อย่างไรก็ตาม เบราว์เซอร์จะให้กำหนดเวลาในการพยายามให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ราบรื่น ดังนั้นคุณควรปฏิบัติตามกำหนดเวลาดังกล่าวเสมอหากไม่มีเหตุผลที่ดี - มีค่าสูงสุดที่
timeRemaining()
จะส่งคืนไหม ใช่ ตอนนี้ขณะนี้เป็นเวลา 50 มิลลิวินาที เมื่อพยายามทำให้แอปพลิเคชันที่ตอบสนองตามอุปกรณ์ การตอบสนองทั้งหมดต่อการโต้ตอบของผู้ใช้ควรอยู่ในระดับไม่เกิน 100 มิลลิวินาที ในกรณีส่วนใหญ่ หากผู้ใช้มีการโต้ตอบตามกรอบเวลา 50 มิลลิวินาที ควรปล่อยให���การเรียกกลับที่ไม่มีการใช้งานเสร็จสมบูรณ์ และให้เบราว์เซอร์ตอบสนองต่อการโต้ตอบของผู้ใช้ได้ คุณอาจได้รับโค้ดเรียกกลับที่ไม่มีการใช้งานหลายรายการติดต่อกัน (หากเบราว์เซอร์ระบุว่ามีเวลาเพียงพอในการเรียกใช้) - มีงานประเภทใดบ้างที่ฉันไม่ควรทำใน requestIdleCallback
โดยหลักการแล้ว งานที่คุณทำควรเป็นงานย่อยๆ (งานย่อย) ที่มีลักษณะที่ค่อนข้างคาดการณ์ได้ ตัวอย่างเช่น การเปลี่ยนแปลง DOM จะมีเวลาดำเนินการที่คาดเดาไม่ได้ เนื่องจากจะทริกเกอร์การคำนวณรูปแบบ เลย์เอาต์ การลงสี และการประสาน ดังนั้นคุณควรทําการเปลี่ยนแปลง DOM ในโค้ดเรียกกลับ
requestAnimationFrame
ตามที่แนะนําด้านบนเท่านั้น อีกสิ่งหนึ่งที่ต้องระวังคือการแก้ (หรือปฏิเสธ) คำสัญญา เนื่องจากโค้ดเรียกกลับจะทำงานทันทีหลังจากที่โค้ดเรียกกลับที่ไม่มีการใช้งานสิ้นสุดลง แม้จะไม่มีเวลาเหลืออยู่แล้วก็ตาม - ฉันจะได้รับ
requestIdleCallback
ที่ตอนท้ายของเฟรมเสมอใช่ไหม ไม่ได้เสมอไป เบราว์เซอร์จะตั้งเวลาเรียกกลับเมื่อมีเวลาว่างเมื่อสิ้นสุดเฟรม หรือในช่วงที่ผู้ใช้ไม่มีการใช้งาน คุณไม่ควรคาดหวังว่าจะมีการเรียกใช้โค้ดเรียกกลับต่อเฟรม และหากต้องการเรียกใช้ภายในกรอบเวลาที่กำหนด คุณควรใช้ประโยชน์จากระยะหมดเวลา - ฉันมีโค้ดเรียกกลับของ
requestIdleCallback
หลายรายการได้ไหม ได้ คุณจะพยายามเรียกกลับด้วยrequestAnimationFrame
ได้หลายครั้ง อย่างไรก็ตาม โปรดทราบว่า หากการเรียกกลับครั้งแรกของคุณใช้เวลาที่เหลืออยู่ในระหว่างการติดต่อกลับก็จะไม่มีเวลาเหลือสำหรับการเรียกกลับอื่นๆ อีก โค้ดเรียกกลับอื่นๆ จะต้องรอจนกว่าเบราว์เซอร์จะไม่มีการใช้งานครั้งถัดไป จึงจะเรียกใช้ได้ การใช้โค้ดเรียกกลับที่ไม่มีการใช้งานครั้งเดียวแล้วแบ่งงานออกเป็นส่วนๆ ขึ้นอยู่กับงานที่คุณพยายามทำให้เสร็จ นอกจากนี้ คุณยังสามารถใช้การหมดเวลาเพื่อให้มั่นใจว่าโค้ดเรียกกลับจะถูกจำกัดเวลา - จะเกิด��ะไรขึ้นหากฉันตั้งค่าโค้ดเรียกกลับที่ไม่มีการใช้งานใหม่ภายในอีกอันหนึ่ง ระบบจะกำหนดเวลาเรียกกลับที่ไม่มีการใช้งานใหม่ให้ทำงานโดยเร็วที่สุด โดยเริ่มจากเฟรมถัดไป (แทนที่จะเป็นเฟรมปัจจุบัน)
ไม่มีการใช้งาน
requestIdleCallback
เป็นวิธีที่ยอดเยี่ยมที่จะตรวจสอบว่าคุณเรียกใช้โค้ดได้ แต่ไม่ขัดขวางผู้ใช้ ใช้งานง่าย และยืดหยุ่นมาก อย่างไรก็ตาม นี่ยังเป็นช่วงเริ่มต้นและยังไม่มีการปิดปรับปรุงข้อกำหนดอย่างสมบูรณ์ เราจึงยินดีรับฟังความคิดเห็นจากคุณ
ลองใช้ฟีเจอร์นี้ใน Chrome Canary, ลองใช้โปรเจ็กต์ของคุณดู และบอกเราว่าคุณจะทำงานได้อย่างไร!