การใช้ requestIdleCallback

เว็บไซต์และแอปจํานวนมากใช้สคริปต์จํานวนมาก บ่อยครั้งที่ JavaScript ต้องทำงานโดยเร็วที่สุด แต่ในขณะเดียวกันคุณก็ไม่ต้องการให้ JavaScript ขัดจังหวะผู้ใช้ หากคุณส่งข้อมูลวิเคราะห์เมื่อผู้ใช้เลื่อนหน้าเว็บ หรือเพิ่มองค์ประกอบต่อท้าย DOM ขณะที่ผู้ใช้แตะปุ่มอยู่ เว็บแอปอาจไม่ตอบสนอง ซึ่งส่งผลให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ไม่ดี

การใช้ requestIdleCallback เพื่อกำหนดเวลางานที่ไม่จำเป็น

ข่าวดีคือตอนนี้เรามี 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 นั้นไม่ค่อยดีนักเนื่องจาก 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 แต่ก็มีความแตกต่างตรงที่รับพารามิเตอร์ที่ 2 ซึ่งเป็นออบเจ็กต์ตัวเลือกที่มีพร็อพเพอร์ตี้ timeout การหมดเวลานี้ (หากตั้งค่าไว้) จะให้เวลาเบราว์เซอร์เป็นมิลลิวินาทีในการเรียกใช้การเรียกกลับ

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

หากการเรียกกลับทํางานเนื่องจากมีการเรียกใช้การหมดเวลา คุณจะเห็น 2 สิ่งต่อไปนี้

  • timeRemaining() จะแสดงผลเป็น 0
  • พร็อพเพอร์ตี้ didTimeout ของออบเจ็กต์ deadline จะเท่ากับ true

หากพบว่า 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 ระบบควรส่งข้อมูลวิเคราะห์ทันที อย่างไรก็ตาม ในแอปพลิเคชันเวอร์ชันที่ใช้งานจริง คุณควรเลื่อนเวลาการส่งด้วยการกำหนดเวลาหมดอายุเพื่อให้แน่ใจว่าจะไม่ขัดแย้งกับการโต้ตอบและทำให้เกิดความกระตุก

การใช้ requestIdleCallback เพื่อทำการเปลี่ยนแปลง DOM

อีกสถานการณ์หนึ่งที่ requestIdleCallback ช่วยเพิ่มประสิทธิภาพได้จริงคือเมื่อคุณต้องทําการเปลี่ยนแปลง DOM ที่ไม่จําเป็น เช่น การเพิ่มรายการที่ท้ายรายการแบบ Lazy Load ที่เพิ่มรายการอยู่เรื่อยๆ มาดูกันว่า requestIdleCallback ใส่ในเฟรมทั่วไปได้อย่างไร

เฟรมทั่วไป

เป็นไปได้ว่าเบราว์เซอร์จะทำงานยุ่งจนไม่สามารถเรียกใช้การเรียกกลับในเฟรมหนึ่งๆ ได้ คุณจึงไม่ควรคาดหวังว่าจะมีเวลาว่างใดๆ ในตอนท้ายของเฟรมเพื่อทำงานเพิ่มเติม ซึ่งแตกต่างจาก setImmediate ที่ทำงานต่อเฟรม

หากการเรียกกลับเริ่มทํางานเมื่อสิ้นสุดเฟรม ระบบจะกําหนดเวลาให้ทํางานหลังจากที่มีการคอมมิตเฟรมปัจจุบัน ซึ่งหมายความว่าจะมีการใช้การเปลี่ยนแปลงสไตล์ และที่สำคัญคือระบบจะคํานวณเลย์เอาต์ หากเราทําการเปลี่ยนแปลง DOM ภายในการเรียกกลับเมื่อไม่มีการใช้งาน การคํานวณเลย์เอาต์เหล่านั้นจะใช้งานไม่ได้ หากมีการอ่านเลย์เอาต์ประเภทใดก็ตามในเฟรมถัดไป เช่น 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 ไหม ขออภัย ไม่ได้ แต่มีชิมหากคุณต้องการการเปลี่ยนเส้นทางไปยัง setTimeout อย่างราบรื่น เหตุผลที่ API นี้มีอยู่ก็เพราะช่วยอุดช่องโหว่ที่แท้จริงในแพลตฟอร์มเว็บ การอนุมานว่าไม่มีกิจกรรมนั้นทําได้ยาก แต่ไม่มี JavaScript API ใดที่จะระบุจํานวนเวลาว่างเมื่อสิ้นสุดเฟรม ดังนั้นคุณจึงต้องเดาเอา คุณสามารถใช้ API เช่น setTimeout, setInterval หรือ setImmediate เพื่อกำหนดเวลาทำงานได้ แต่ API เหล่านี้ไม่ได้กำหนดเวลาเพื่อหลีกเลี่ยงการโต้ตอบของผู้ใช้ในลักษณะเดียวกับ requestIdleCallback
  • จะเกิดอะไรขึ้นหากฉันส่งไม่ทันกำหนดเวลา หาก timeRemaining() แสดงผลเป็น 0 แต่คุณเลือกที่จะเรียกใช้เป็นเวลานานขึ้น คุณก็ทําได้โดยไม่ต้องกลัวว่าเบราว์เซอร์จะหยุดทํางาน อย่างไรก็ตาม เบราว์เซอร์จะกำหนดกำหนดเวลาให้คุณเพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ราบรื่น ดังนั้นคุณควรปฏิบัติตามกำหนดเวลาเสมอ เว้นแต่จะมีเหตุผลอันควร
  • timeRemaining() จะแสดงค่าสูงสุดไหม ใช่ ตอนนี้อยู่ที่ 50 มิลลิวินาที เมื่อพยายามรักษาแอปพลิเคชันที่ตอบสนองได้ดี การตอบสนองทั้งหมดต่อการโต้ตอบของผู้ใช้ควรใช้เวลาไม่เกิน 100 มิลลิวินาที ในกรณีส่วนใหญ่ หากผู้ใช้โต้ตอบ กรอบเวลา 50 มิลลิวินาทีควรอนุญาตให้การเรียกกลับเมื่อไม่มีการใช้งานเสร็จสมบูรณ์ และเพื่อให้เบราว์เซอร์ตอบสนองต่อการโต้ตอบของผู้ใช้ คุณอาจได้รับการเรียกกลับแบบไม่มีการใช้งานหลายรายการที่กำหนดเวลาไว้ติดต่อกัน (หากเบราว์เซอร์พิจารณาว่ามีเวลาเพียงพอที่จะเรียกใช้)
  • มีงานประเภทใดบ้างที่ฉันไม่ควรทำใน requestIdleCallback โดยปกติแล้วงานที่คุณทำควรแบ่งออกเป็นชิ้นเล็กๆ (ไมโครแทสก์) ที่มีลักษณะที่คาดการณ์ได้ค่อนข้างดี ตัวอย่างเช่น การเปลี่ยนแปลง DOM โดยเฉพาะจะมีเวลาดำเนินการที่ไม่อาจคาดเดาได้ เนื่องจากจะทริกเกอร์การคำนวณสไตล์ เลย์เอาต์ การวาดภาพ และการคอมโพส คุณจึงควรทำการเปลี่ยนแปลง DOM ใน requestAnimationFrame callback ตามที่แนะนำไว้ด้านบนเท่านั้น อีกสิ่งที่ควรระวังคือการแก้ไข (หรือปฏิเสธ) Promise เนื่องจากระบบจะเรียกใช้การเรียกกลับทันทีหลังจากที่การเรียกกลับแบบไม่มีการใช้งานเสร็จสิ้นแล้ว แม้ว่าจะไม่มีเวลาเหลืออยู่ก็ตาม
  • ฉันจะได้รับ requestIdleCallback ที่ท้ายเฟรมเสมอไหม ไม่เสมอไป เบราว์เซอร์จะกําหนดเวลาการเรียกกลับทุกครั้งที่มีเวลาว่างเมื่อสิ้นสุดเฟรม หรือในช่วงเวลาที่ผู้ใช้ไม่ได้ใช้งาน คุณไม่ควรคาดหวังว่าจะมีการเรียกใช้การเรียกกลับต่อเฟรม และหากต้องการให้การเรียกใช้ทำงานภายในกรอบเวลาที่กำหนด คุณควรใช้การหมดเวลา
  • ฉันจะเรียกใช้ requestIdleCallback หลายรายการพร้อมกันได้ไหม ได้ คุณสามารถตั้งค่าrequestAnimationFrameการโทรกลับได้หลายรายการ แต่โปรดทราบว่าหากการเรียกกลับครั้งแรกใช้เวลาที่เหลืออยู่ระหว่างการเรียกกลับจนหมด ก็จะไม่มีเวลาเหลือสำหรับการเรียกกลับอื่นๆ อีก จากนั้นการเรียกกลับอื่นๆ จะต้องรอจนกว่าเบราว์เซอร์จะหยุดทำงานในครั้งถัดไปจึงจะเรียกใช้ได้ คุณอาจใช้การเรียกกลับแบบไม่มีการใช้งานเพียงรายการเดียวและแบ่งงานในนั้น ทั้งนี้ขึ้นอยู่กับงานที่ต้องการทํา หรือจะใช้การหมดเวลาเพื่อให้แน่ใจว่าการเรียกกลับจะไม่ขาดเวลา
  • จะเกิดอะไรขึ้นหากฉันตั้งค่าการเรียกกลับใหม่เมื่อไม่มีการใช้งานภายในการเรียกกลับอื่น ระบบจะกําหนดเวลาให้คอลแบ็กใหม่เมื่อไม่มีการใช้งานทำงานโดยเร็วที่สุด โดยเริ่มจากเฟรมถัดไป (ไม่ใช่เฟรมปัจจุบัน)

ไปกันต่อเลย

requestIdleCallback เป็นวิธีที่ยอดเยี่ยมในการทำให้แน่ใจว่าคุณจะเรียกใช้โค้ดได้ โดยไม่รบกวนผู้ใช้ ใช้งานง่ายและยืดหยุ่นมาก อย่างไรก็ตาม เรายังอยู่ในขั้นเริ่มต้นและข้อกำหนดยังไม่เสร็จสมบูรณ์ เรายินดีรับฟังความคิดเห็นจากคุณ

ลองใช้ฟีเจอร์นี้ใน Chrome Canary แล้วนำไปใช้กับโปรเจ็กต์ของคุณ แล้วบอกให้เราทราบถึงผลลัพธ์ที่ได้