การใช้ requestIdleCallback

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

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

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