Menggunakan requestIdleCallback

Banyak situs dan aplikasi memiliki banyak skrip untuk dieksekusi. JavaScript sering kali perlu dijalankan sesegera mungkin, tapi pada saat yang sama, Anda tidak ingin menghalangi pengguna. Jika Anda mengirim data analisis saat pengguna men-scroll halaman, atau Anda menambahkan elemen ke DOM saat tombol tersebut diketuk, aplikasi web Anda bisa menjadi tidak responsif dan mengakibatkan pengalaman pengguna yang buruk.

Menggunakan requestIdleCallback untuk menjadwalkan pekerjaan yang tidak penting.

Kabar baiknya, sekarang ada API yang dapat membantu: requestIdleCallback. Dengan cara yang sama seperti penggunaan requestAnimationFrame yang memungkinkan kita menjadwalkan animasi dengan benar dan memaksimalkan peluang untuk mencapai 60 fps, requestIdleCallback akan menjadwalkan pekerjaan saat ada waktu luang di akhir frame, atau saat pengguna tidak aktif. Ini berarti ada peluang untuk melakukan pekerjaan Anda tanpa menghalangi pengguna. Fitur ini tersedia mulai Chrome 47, jadi Anda dapat mencobanya sekarang dengan menggunakan Chrome Canary. Fitur ini merupakan fitur eksperimental, dan spesifikasinya masih berubah, sehingga dapat berubah di masa mendatang.

Mengapa saya harus menggunakan requestIdleCallback?

Menjadwalkan pekerjaan yang tidak penting sendiri sangat sulit dilakukan. Tidak mungkin untuk mengetahui dengan tepat berapa banyak waktu render frame yang tersisa karena setelah callback requestAnimationFrame dieksekusi ada penghitungan gaya, tata letak, penggambaran, dan bagian dalam browser lain yang perlu dijalankan. Solusi buatan sendiri tidak dapat memperhitungkan semua hal tersebut. Untuk memastikan pengguna tidak berinteraksi dengan cara tertentu, Anda juga harus mengaitkan pemroses ke setiap jenis peristiwa interaksi (scroll, touch, click), hanya jika Anda tidak membutuhkannya untuk fungsi, hanya agar Anda dapat benar-benar yakin bahwa pengguna tidak berinteraksi. Di sisi lain, browser tahu persis berapa banyak waktu yang tersedia di akhir frame, dan apakah pengguna berinteraksi, sehingga melalui requestIdleCallback kita mendapatkan API yang memungkinkan kita memanfaatkan waktu luang dengan cara yang paling efisien.

Mari kita lihat sedikit lebih detail dan lihat bagaimana kita dapat memanfaatkannya.

Memeriksa requestIdleCallback

requestIdleCallback masih baru, jadi sebelum menggunakannya, Anda harus memeriksa apakah fitur tersebut tersedia untuk digunakan:

if ('requestIdleCallback' in window) {
    // Use requestIdleCallback to schedule work.
} else {
    // Do what you’d do today.
}

Anda juga dapat melakukan shim pada perilakunya, yang mengharuskan untuk kembali ke 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);
    }

Menggunakan setTimeout tidak bagus karena tidak mengetahui waktu tidak ada aktivitas seperti yang dilakukan requestIdleCallback, tetapi karena Anda akan memanggil fungsi secara langsung jika requestIdleCallback tidak tersedia, Anda tidak akan mengalami shimming dengan cara ini lebih buruk. Dengan shim, jika requestIdleCallback tersedia, panggilan Anda akan dialihkan tanpa suara, dan itu bagus.

Namun, untuk saat ini, mari kita asumsikan hal itu ada.

Menggunakan requestIdleCallback

Memanggil requestIdleCallback sangat mirip dengan requestAnimationFrame karena menggunakan fungsi callback sebagai parameter pertamanya:

requestIdleCallback(myNonEssentialWork);

Saat myNonEssentialWork dipanggil, objek tersebut akan diberi objek deadline yang berisi fungsi yang menampilkan angka yang menunjukkan berapa banyak waktu yang tersisa untuk pekerjaan Anda:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

Fungsi timeRemaining dapat dipanggil untuk mendapatkan nilai terbaru. Saat timeRemaining() menampilkan nol, Anda dapat menjadwalkan requestIdleCallback lainnya jika masih ada pekerjaan lain yang harus dilakukan:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

Memastikan fungsi Anda dipanggil

Apa yang Anda lakukan jika segala sesuatu sangat sibuk? Anda mungkin khawatir bahwa callback mungkin tidak akan pernah dipanggil. Meskipun mirip dengan requestAnimationFrame, requestIdleCallback juga berbeda karena memerlukan parameter kedua opsional: objek opsi dengan properti waktu tunggu. Waktu tunggu ini, jika disetel, akan memberi browser waktu dalam milidetik saat browser harus mengeksekusi callback:

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

Jika callback Anda dieksekusi karena waktu tunggu habis, Anda akan melihat dua hal:

  • timeRemaining() akan menampilkan nol.
  • Properti didTimeout dari objek deadline akan bernilai benar (true).

Jika didTimeout sudah benar, kemungkinan besar Anda hanya ingin menjalankan pekerjaan dan menyelesaikannya:

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);
}

Karena potensi gangguan pada waktu tunggu yang dapat menyebabkan waktu tunggu ini bagi pengguna Anda (pekerjaan dapat menyebabkan aplikasi menjadi tidak responsif atau tersendat), berhati-hatilah saat menyetel parameter ini. Jika bisa, biarkan browser memutuskan kapan harus memanggil callback.

Menggunakan requestIdleCallback untuk mengirim data analisis

Mari kita lihat cara menggunakan requestIdleCallback untuk mengirim data analisis. Dalam hal ini, kita mungkin ingin melacak peristiwa seperti -- katakan -- mengetuk menu navigasi. Namun, karena peristiwa tersebut biasanya dianimasikan ke layar, sebaiknya kita tidak langsung mengirim peristiwa ini ke Google Analytics. Kita akan membuat array peristiwa untuk dikirim dan meminta agar peristiwa tersebut dikirim pada waktu tertentu di masa mendatang:

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();
}

Sekarang kita perlu menggunakan requestIdleCallback untuk memproses peristiwa yang tertunda:

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();
    }
}

Di sini Anda dapat melihat saya telah menetapkan waktu tunggu 2 detik, tetapi nilai ini akan bergantung pada aplikasi Anda. Untuk data analisis, waktu tunggu akan digunakan untuk memastikan data dilaporkan dalam jangka waktu yang wajar, dan bukan hanya pada beberapa waktu di masa mendatang.

Terakhir, kita harus menulis fungsi yang akan dijalankan 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();
}

Untuk contoh ini, saya berasumsi bahwa jika requestIdleCallback tidak ada, data analisis harus segera dikirim. Namun, dalam aplikasi produksi, akan lebih baik untuk menunda pengiriman dengan waktu tunggu untuk memastikannya tidak bertentangan dengan interaksi apa pun dan menyebabkan jank.

Menggunakan requestIdleCallback untuk membuat perubahan DOM

Situasi lain saat requestIdleCallback benar-benar dapat membantu performa adalah saat Anda memiliki perubahan DOM yang tidak penting untuk dilakukan, seperti menambahkan item ke bagian akhir daftar yang lambat dimuat dan terus bertambah. Mari kita lihat cara requestIdleCallback sesuai dengan frame biasa.

Frame yang biasa.

Ada kemungkinan browser akan terlalu sibuk untuk menjalankan callback dalam frame tertentu, sehingga kira-kira akan ada waktu luang setiap waktu di akhir frame untuk melakukan tugas lagi. Hal ini membuatnya berbeda dengan setImmediate, yang benar-benar berjalan per frame.

Jika callback diaktifkan di akhir frame, callback akan dijadwalkan untuk berjalan setelah frame saat ini di-commit, yang berarti bahwa perubahan gaya akan diterapkan, dan, yang penting, tata letak dihitung. Jika kita membuat perubahan DOM di dalam callback tidak ada aktivitas, penghitungan tata letak tersebut akan menjadi tidak valid. Jika ada jenis pembacaan tata letak di frame berikutnya, mis. getBoundingClientRect, clientWidth, dll., browser harus menjalankan Tata Letak Sinkron Paksa, yang berpotensi menyebabkan bottleneck performa.

Alasan lain untuk tidak memicu perubahan DOM dalam callback tidak ada aktivitas adalah bahwa dampak waktu dari perubahan DOM tidak dapat diprediksi, dan dengan demikian kita bisa dengan mudah melewati batas waktu yang diberikan browser.

Praktik terbaiknya adalah hanya membuat perubahan DOM di dalam callback requestAnimationFrame, karena sudah dijadwalkan oleh browser dengan mempertimbangkan jenis pekerjaan tersebut. Artinya, kode kita harus menggunakan fragmen dokumen, yang kemudian dapat ditambahkan dalam callback requestAnimationFrame berikutnya. Jika menggunakan library VDOM, Anda perlu menggunakan requestIdleCallback untuk membuat perubahan, tetapi harus menerapkan patch DOM di callback requestAnimationFrame berikutnya, bukan callback tidak ada aktivitas.

Dengan mengingat hal itu, mari kita lihat kodenya:

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();
}

Di sini saya membuat elemen dan menggunakan properti textContent untuk mengisinya, tetapi kemungkinan kode pembuatan elemen Anda akan lebih rumit. Setelah membuat elemen, scheduleVisualUpdateIfNeeded dipanggil, yang akan menyiapkan satu callback requestAnimationFrame yang akan menambahkan fragmen dokumen ke isi:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

Semuanya baik-baik saja, sekarang kita akan melihat jauh lebih sedikit jank saat menambahkan item ke DOM. Sempurna!

FAQ

  • Apakah ada polyfill? Sayangnya tidak, tetapi ada shim jika Anda ingin memiliki pengalihan yang transparan ke setTimeout. Alasan ada API ini adalah karena API ini mengisi kesenjangan yang sangat nyata di platform web. Menyimpulkan kurangnya aktivitas itu sulit, tetapi tidak ada JavaScript API untuk menentukan jumlah waktu luang di akhir bingkai, jadi paling baik Anda harus membuat tebakan. API seperti setTimeout, setInterval, atau setImmediate dapat digunakan untuk menjadwalkan pekerjaan, tetapi tidak diatur waktunya untuk menghindari interaksi pengguna seperti requestIdleCallback.
  • Apa yang terjadi jika saya melebihi batas waktu? Jika timeRemaining() menampilkan nol, tetapi Anda memilih untuk menjalankannya lebih lama, Anda dapat melakukannya tanpa khawatir browser menghentikan pekerjaan Anda. Namun, browser memberi Anda tenggat waktu untuk mencoba dan memastikan pengalaman yang lancar bagi pengguna. Jadi, kecuali ada alasan yang sangat bagus, Anda harus selalu mematuhi tenggat waktu.
  • Apakah ada nilai maksimum yang akan ditampilkan timeRemaining()? Ya, saat ini 50 md. Saat mencoba mempertahankan aplikasi yang responsif, semua respons terhadap interaksi pengguna harus dijaga di bawah 100 md. Jika pengguna berinteraksi selama periode 50 md, dalam sebagian besar kasus, harus memungkinkan callback tidak ada aktivitas selesai, dan browser merespons interaksi pengguna. Anda mungkin mendapatkan beberapa callback tidak ada aktivitas yang dijadwalkan secara berurutan (jika browser menentukan bahwa ada cukup waktu untuk menjalankannya).
  • Apakah ada pekerjaan yang tidak boleh saya lakukan dalam requestIdleCallback? Idealnya, pekerjaan yang Anda lakukan harus dalam potongan kecil (microtask) yang memiliki karakteristik yang relatif dapat diprediksi. Misalnya, mengubah DOM secara khusus akan memiliki waktu eksekusi yang tidak dapat diprediksi, karena akan memicu penghitungan gaya, tata letak, penggambaran, dan pengomposisian. Oleh karena itu, Anda hanya boleh membuat perubahan DOM dalam callback requestAnimationFrame seperti yang disarankan di atas. Hal lain yang harus diwaspadai adalah menyelesaikan (atau menolak) Promise, karena callback akan dieksekusi segera setelah callback tidak ada aktivitas selesai, meskipun tidak ada lagi waktu yang tersisa.
  • Apakah saya akan selalu mendapatkan requestIdleCallback di akhir frame? Tidak, tidak selalu. Browser akan menjadwalkan callback setiap kali ada waktu luang di akhir frame, atau dalam periode saat pengguna tidak aktif. Jangan berharap callback dipanggil per frame, dan jika Anda mengharuskannya untuk berjalan dalam jangka waktu tertentu, Anda harus memanfaatkan waktu tunggu.
  • Dapatkah saya memiliki beberapa callback requestIdleCallback? Ya, Anda bisa. Memang Anda bisa memiliki beberapa callback requestAnimationFrame. Namun, perlu diingat bahwa jika callback pertama Anda menggunakan waktu yang tersisa selama callback-nya, maka tidak akan ada lagi waktu yang tersisa untuk callback lainnya. Callback lainnya kemudian harus menunggu sampai browser tidak ada aktivitas lagi sebelum bisa dijalankan. Bergantung pada pekerjaan yang ingin Anda selesaikan, mungkin lebih baik menggunakan satu callback nonaktif dan membagi pekerjaan di sana. Atau, Anda bisa memanfaatkan waktu tunggu untuk memastikan tidak ada callback yang kehabisan waktu.
  • Apa yang terjadi jika saya menetapkan callback tidak ada aktivitas baru di dalam callback lain? Callback tidak ada aktivitas yang baru akan dijadwalkan untuk berjalan sesegera mungkin, mulai dari frame berikutnya (bukan yang saat ini).

Menganggur!

requestIdleCallback adalah cara yang efektif untuk memastikan Anda dapat menjalankan kode, tanpa menghalangi pengguna. {i>Tool<i} ini mudah digunakan, dan sangat fleksibel. Ini masih tahap awal, dan spesifikasinya belum sepenuhnya ditetapkan, jadi terimalah masukan dari Anda.

Lihat di Chrome Canary, coba proyek Anda, dan beri tahu kami cara Anda melakukannya.