เว็บไซต์และแอปจำนวนมากมีสคริปต์ให้เรียกใช้เป็นจำนวนมาก บ่อยครั้งที่ JavaScript ต้องทำงานโดยเร็วที่สุด แต่ในขณะเดียวกันคุณก็ไม่ต้องการให้ JavaScript ขัดจังหวะผู้ใช้ หากคุณส่งข้อมูลการวิเคราะห์เมื่อผู้ใช้เลื่อนหน้าเว็บ หรือคุณผนวกองค์ประกอบลงใน DOM ขณะที่แตะอยู่บนปุ่ม เว็บแอปอาจไม่ตอบสนอง ซึ่งส่งผลให้ผู้ใช้ได้รับประสบการณ์ที่ไม่ดี
ข่าวดีคือตอนนี้เรามี API ที่จะช่วยคุณในเรื่องต่อไปนี้ requestIdleCallback
ในทำนองเดียวกับที่การใช้ requestAnimationFrame
ช่วยให้เรากำหนดเวลาภาพเคลื่อนไหวได้อย่างเหมาะสมและเพิ่มโอกาสในการเล่นที่ 60 fps ให้ได้สูงสุด requestIdleCallback
จะกำหนดเวลาทำงานเมื่อมีเวลาว่างเมื่อสิ้นสุดเฟรมหรือเมื่อผู้ใช้ไม่ได้ใช้งาน ซึ่งหมายความว่าคุณมีโอกาสทำงานโดยไม่ต้องไปขัดขวางการทำงานของผู้ใช้ ปัจจุบันมีให้ใช้งานใน Chrome 47 แล้ว ลองใช้ Chrome Canary เพื่อทดลองใช้วันนี้เลย ฟีเจอร์นี้เป็นฟีเจอร์ทดลองและข้อมูลจำเพาะยังอยู่ระหว่างการเปลี่ยนแปลง จึงอาจมีการเปลี่ยนแปลงในอนาคต
เหตุใดฉันจึงควรใช้ requestIdleCallback
การกำหนดเวลางานที่ไม่จำเป็นด้วยตนเองนั้นทำได้ยากมาก คุณจะไม่สามารถคำนวณเวลาเฟรมที่เหลือได้อย่างแม่นยำ เนื่องจากหลังจากการเรียก requestAnimationFrame
กลับจะต้องมีการคํานวณสไตล์ เลย์เอาต์ การวาด และข้อมูลภายในอื่นๆ ของเบราว์เซอร์ที่จำเป็นต้องทำงาน โซลูชันที่ติดตั้งเองไม่สามารถรองรับสิ่งเหล่านี้ หากต้องการแน่ใจว่าผู้ใช้ไม่ได้โต้ตอบด้วยวิธีใดวิธีหนึ่ง คุณจะต้องแนบ Listener กับเหตุการณ์การโต้ตอบทุกประเภท (scroll
, touch
, click
) ด้วย แม้ว่าจะไม่ต้องใช้ Listener เหล่านั้นเพื่อฟังก์ชันการทำงาน เพียงเพื่อให้แน่ใจว่าผู้ใช้ไม่ได้โต้ตอบ ในทางกลับกัน เบราว์เซอร์จะทราบเวลาที่เหลืออยู่เมื่อสิ้นสุดเฟรม และทราบว่าผู้ใช้กำลังโต้ตอบอยู่หรือไม่ ดังนั้น requestIdleCallback
จึงทำให้เราได้รับ API ที่ช่วยให้ใช้ประโยชน์จากเวลาที่เหลืออยู่ได้อย่างมีประสิทธิภาพมากที่สุด
มาดูรายละเอียดเพิ่มเติมและวิธีใช้ประโยชน์กัน
กำลังตรวจสอบ requestIdleCallback
วันนี้เป็นช่วงแรกๆ ของ requestIdleCallback
ดังนั้นก่อนที่จะใช้งาน โปรดตรวจสอบว่าบัตรพร้อมใช้งานแล้ว ดังนี้
if ('requestIdleCallback' in window) {
// Use requestIdleCallback to schedule work.
} else {
// Do what you’d do today.
}
นอกจากนี้ คุณยังปรับลักษณะการทํางานของ setTimeout
ได้ด้วย โดยจะต้องเปลี่ยนไปใช้ 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()
แสดงผลเป็น 0 คุณสามารถกําหนดเวลา requestIdleCallback
ใหม่ได้หากยังมีงานเหลืออยู่ โดยทําดังนี้
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
การรับประกันว่ามีการเรียกใช้ฟังก์ชัน
คุณจะทำอย่างไรหากมีงานล้นมือ คุณอาจกังวลว่าอาจไม่มีการโทรติดต่อกลับของคุณ แม้ว่า requestIdleCallback
จะคล้ายกับ requestAnimationFrame
แต่ก็มีความแตกต่างตรงที่ requestIdleCallback
จะใช้พารามิเตอร์ที่ 2 ที่ไม่บังคับ ซึ่งเป็นออบเจ็กต์ตัวเลือกที่มีพร็อพเพอร์ตี้ timeout ระยะหมดเวลานี้หากตั้งค่าไว้ จะให้เวลาเบราว์เซอร์เป็นมิลลิวินาทีที่เบราว์เซอร์จะต้องเรียกใช้ Callback ต่อไปนี้
// 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 Load ที่เพิ่มรายการอยู่ตลอดเวลา มาดูกันว่า requestIdleCallback
ใส่ในเฟรมทั่วไปได้อย่างไร
เบราว์เซอร์อาจไม่ว่างที่จะเรียกใช้ Callback ในเฟรมหนึ่งๆ ดังนั้น คุณจึงไม่ควรคาดหวังว่าจะมีเวลาว่างใดๆ ในตอนท้ายของเฟรมเพื่อทำงานอื่นเพิ่มเติม ซึ่งแตกต่างจาก setImmediate
ที่ทำงานต่อเฟรม
หากการเรียกกลับเริ่มทํางานเมื่อสิ้นสุดเฟรม ระบบจะกําหนดเวลาให้ทํางานหลังจากที่มีการคอมมิตเฟรมปัจจุบัน ซึ่งหมายความว่าจะมีการใช้การเปลี่ยนแปลงสไตล์ และที่สำคัญคือระบบจะคํานวณเลย์เอาต์ หากเราทำการเปลี่ยนแปลง DOM ภายใน Callback ที่ไม่มีการใช้งาน การคำนวณเลย์เอาต์เหล่านั้นจะใช้ไม่ได้ หากมีการอ่านเลย์เอาต์ประเภทใดก็ตามในเฟรมถัดไป เช่น getBoundingClientRect
, clientWidth
เป็นต้น เบราว์เซอร์จะต้องทำเลย์เอาต์แบบซิงโครนัสแบบบังคับ ซึ่งอาจเป็นจุดคอขวดของประสิทธิภาพที่อาจเกิดขึ้น
อีกเหตุผลหนึ่งที่ไม่ทริกเกอร์การเปลี่ยนแปลง DOM ในการเรียกกลับเมื่อไม่มีการใช้งานคือผลกระทบด้านเวลาของการเปลี่ยนแปลง DOM นั้นคาดเดาไม่ได้ และเราอาจเลยกำหนดเวลาที่เบราว์เซอร์ระบุไว้ได้ง่ายๆ
แนวทางปฏิบัติแนะนำคือทำการเปลี่ยนแปลง DOM เฉพาะภายในการเรียกกลับ requestAnimationFrame
เนื่องจากเบราว์เซอร์จะกำหนดเวลาให้การดำเนินการประเภทนั้น ซึ่งหมายความว่าโค้ดของเราต้องใช้ชิ้นส่วนเอกสาร ซึ่งจะเพิ่มต่อท้ายใน requestAnimationFrame
callback ถัดไป หากใช้ไลบรารี VDOM คุณจะใช้ requestIdleCallback
เพื่อทำการเปลี่ยนแปลง แต่จะต้องใช้แพตช์ DOM ใน requestAnimationFrame
callback ถัดไป ไม่ใช่ idle callback
เราจะมาดูโค้ดกันเลย
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 ไหม
แย่จัง แต่มี Shim หากคุณต้องการให้เปลี่ยนเส้นทางไปยัง
setTimeout
อย่างโปร่งใส เหตุผลที่ API นี้มีอยู่ก็เพราะช่วยอุดช่องโหว่ที่แท้จริงในแพลตฟอร์มเว็บ การอนุมานการไม่มีกิจกรรมนั้นทำได้ยาก แต่ยังไม่มี JavaScript API ที่จะระบุจำนวนเวลาว่างในตอนท้ายของเฟรม ดังนั้นวิธีที่ดีที่สุดคือคุณต้องคาดเดา API เช่นsetTimeout
,setInterval
หรือsetImmediate
สามารถใช้เพื่อกำหนดเวลางานได้ แต่ต้องไม่มีการกำหนดเวลาเพื่อหลีกเลี่ยงการโต้ตอบของผู้ใช้ในลักษณะที่requestIdleCallback
- จะเกิดอะไรขึ้นหากฉันใช้เกินกำหนดเวลา
หาก
timeRemaining()
แสดงผลเป็น 0 แต่คุณเลือกที่จะเรียกใช้เป็นเวลานานขึ้น คุณก็ทําได้โดยไม่ต้องกลัวว่าเบราว์เซอร์จะหยุดทํางาน อย่างไรก็ตาม เบราว์เซอร์จะกำหนดกำหนดเวลาให้คุณเพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ราบรื่น ดังนั้นคุณควรปฏิบัติตามกำหนดเวลาเสมอ เว้นแต่จะมีเหตุผลอันควร timeRemaining()
จะแสดงค่าสูงสุดไหม ใช่ ตอนนี้ตอนนี้อยู่ที่ 50 มิลลิวินาที เมื่อพยายามทำให้แอปพลิเคชันที่ตอบสนองตามอุปกรณ์ การตอบสนองทั้งหมดต่อการโต้ตอบของผู้ใช้ควรไม่เกิน 100 มิลลิวินาที ในกรณีส่วนใหญ่ หากผู้ใช้โต้ตอบ กรอบเวลา 50 มิลลิวินาทีควรอนุญาตให้การเรียกกลับเมื่อไม่มีการใช้งานเสร็จสมบูรณ์ และเพื่อให้เบราว์เซอร์ตอบสนองต่อการโต้ตอบของผู้ใช้ คุณอาจได้รับการเรียกกลับแบบไม่มีการใช้งานหลายรายการที่กำหนดเวลาไว้ติดต่อกัน (หากเบราว์เซอร์พิจารณาว่ามีเวลาเพียงพอที่จะเรียกใช้)- มีงานประเภทใดบ้างที่ฉันไม่ควรทำใน requestIdleCallback
โดยควรแบ่งงานออกเป็นชิ้นเล็กๆ (ไมโครแทสก์) ที่มีลักษณะที่คาดการณ์ได้ ตัวอย่างเช่น การเปลี่ยนแปลง DOM โดยเฉพาะจะมีเวลาดำเนินการที่ไม่อาจคาดเดาได้ เนื่องจากจะทริกเกอร์การคำนวณสไตล์ เลย์เอาต์ การวาดภาพ และการคอมโพส คุณจึงควรทำการเปลี่ยนแปลง DOM ใน
requestAnimationFrame
callback ตามที่แนะนำไว้ด้านบนเท่านั้น อีกสิ่งที่ควรระวังคือการแก้ไข (หรือปฏิเสธ) Promise เนื่องจากระบบจะเรียกใช้การเรียกกลับทันทีหลังจากที่การเรียกกลับแบบไม่มีการใช้งานเสร็จสิ้นแล้ว แม้ว่าจะไม่มีเวลาเหลืออยู่ก็ตาม - ฉันจะได้รับ
requestIdleCallback
ที่ท้ายเฟรมเสมอไหม ไม่เสมอไป เบราว์เซอร์จะกําหนดเวลาการเรียกกลับทุกครั้งที่มีเวลาว่างเมื่อสิ้นสุดเฟรม หรือในช่วงเวลาที่ผู้ใช้ไม่ได้ใช้งาน คุณไม่ควรคาดหวังว่าจะมีการเรียก Callback ต่อเฟรม และหากคุณต้องการให้ Callback ทำงานภายในกรอบเวลาที่กำหนด คุณควรใช้ประโยชน์จากการหมดเวลา - ฉันมี
requestIdleCallback
หลายรายการที่เรียกกลับได้ไหม ได้ คุณสามารถตั้งค่าrequestAnimationFrame
การโทรกลับได้หลายรายการ แต่โปรดทราบว่าหากการเรียกกลับครั้งแรกใช้เวลาที่เหลืออยู่ระหว่างการเรียกกลับจนหมด ก็จะไม่มีเวลาเหลือสำหรับการเรียกกลับอื่นๆ อีก จากนั้นการเรียกกลับอื่นๆ จะต้องรอจนกว่าเบราว์เซอร์จะหยุดทำงานในครั้งถัดไปจึงจะเรียกใช้ได้ คุณควรมีการเรียกกลับเมื่อไม่มีการใช้งานครั้งเดียวแล้วแบ่งงานออกจากกัน โดยขึ้นอยู่กับงานที่คุณพยายามทำ หรือจะใช้การหมดเวลาเพื่อให้แน่ใจว่าการเรียกกลับจะไม่ขาดเวลา - จะเกิดอะไรขึ้นหากฉันตั้งค่าการเรียกกลับใหม่เมื่อไม่มีการใช้งานภายในการเรียกกลับอื่น ระบบจะกําหนดเวลาให้การเรียกกลับใหม่เมื่อไม่มีการใช้งานทํางานโดยเร็วที่สุด โดยเริ่มจากเฟรมถัดไป (แทนที่จะเป็นเฟรมปัจจุบัน)
ไม่มีการใช้งาน
requestIdleCallback
เป็นวิธีที่ยอดเยี่ยมในการทำให้แน่ใจว่าคุณจะเรียกใช้โค้ดได้ โดยไม่รบกวนผู้ใช้ ใช้งานง่ายและยืดหยุ่นมาก แต่เรายังอยู่ในขั้นเริ่มต้นและข้อกำหนดยังไม่เสร็จสมบูรณ์ เรายินดีรับฟังความคิดเห็นจากคุณ
ลองใช้ฟีเจอร์นี้ใน Chrome Canary แล้วนำไปใช้กับโปรเจ็กต์ของคุณ แล้วบอกให้เราทราบถึงผลลัพธ์ที่ได้