Menggunakan requestIdleCallback

Banyak situs dan aplikasi memiliki banyak skrip yang harus dieksekusi. JavaScript sering kali perlu dijalankan sesegera mungkin, tetapi 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 pengguna mengetuk tombol, aplikasi web Anda dapat menjadi tidak responsif, sehingga menghasilkan pengalaman pengguna yang buruk.

Menggunakan requestIdleCallback untuk menjadwalkan pekerjaan yang tidak penting.

Kabar baiknya adalah sekarang ada API yang dapat membantu: requestIdleCallback. Dengan cara yang sama seperti mengadopsi 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. Artinya, ada peluang untuk melakukan pekerjaan Anda tanpa mengganggu pengguna. Fitur ini tersedia mulai Chrome 47, jadi Anda dapat mencobanya sekarang dengan menggunakan Chrome Canary. Ini adalah fitur eksperimental, dan spesifikasinya masih berubah-ubah, sehingga hal-hal dapat berubah pada 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 internal browser lainnya yang perlu dijalankan. Solusi buatan sendiri tidak dapat memperhitungkan hal-hal tersebut. Untuk memastikan pengguna tidak berinteraksi dengan cara tertentu, Anda juga harus mengaitkan pemroses ke setiap jenis peristiwa interaksi (scroll, touch, click), meskipun Anda tidak memerlukannya untuk fungsionalitasnya, hanya agar Anda 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 lebih mendetail dan melihat cara menggunakannya.

Memeriksa requestIdleCallback

requestIdleCallback masih dalam tahap awal, jadi sebelum menggunakannya, Anda harus memeriksa apakah requestIdleCallback tersedia untuk digunakan:

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

Anda juga dapat mengganti perilakunya, yang mengharuskan 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 bahwa kode tersebut 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. Jika timeRemaining() menampilkan nol, Anda dapat menjadwalkan requestIdleCallback lain jika masih ada pekerjaan 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 semuanya sangat sibuk? Anda mungkin khawatir bahwa callback Anda mungkin tidak pernah dipanggil. Meskipun requestIdleCallback menyerupai requestAnimationFrame, requestIdleCallback juga berbeda karena memerlukan parameter kedua opsional: objek opsi dengan properti waktu tunggu. Waktu tunggu ini, jika ditetapkan, memberi browser waktu dalam milidetik yang harus digunakan untuk menjalankan callback:

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

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

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

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 yang dapat ditimbulkan oleh waktu tunggu ini kepada pengguna (pekerjaan dapat menyebabkan aplikasi Anda tidak responsif atau mengalami jank), berhati-hatilah saat menetapkan parameter ini. Jika memungkinkan, izinkan browser menentukan 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 -- misalnya -- mengetuk menu navigasi. Namun, karena biasanya animasi ini muncul di layar, sebaiknya jangan langsung mengirim peristiwa ini ke Google Analytics. Kita akan membuat array peristiwa untuk dikirim dan meminta agar peristiwa tersebut dikirim pada suatu waktu 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 harus 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 bahwa saya telah menetapkan waktu tunggu 2 detik, tetapi nilai ini akan bergantung pada aplikasi Anda. Untuk data analisis, wajar jika waktu tunggu akan digunakan untuk memastikan data dilaporkan dalam jangka waktu yang wajar, bukan hanya pada suatu waktu di masa mendatang.

Terakhir, kita perlu menulis fungsi yang akan dieksekusi 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 mengasumsikan bahwa jika requestIdleCallback tidak ada, data analisis harus segera dikirim. Namun, dalam aplikasi produksi, sebaiknya tunda 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 perlu melakukan perubahan DOM yang tidak penting, seperti menambahkan item ke akhir daftar yang dimuat lambat dan terus bertambah. Mari kita lihat bagaimana requestIdleCallback sebenarnya sesuai dengan bingkai standar.

Bingkai standar.

Kemungkinan browser akan terlalu sibuk untuk menjalankan callback dalam frame tertentu, sehingga kira-kira akan ada setiap waktu luang 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 dijalankan setelah frame saat ini di-commit, yang berarti perubahan gaya akan diterapkan, dan yang terpenting, 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, misalnya getBoundingClientRect, clientWidth, dll., browser harus melakukan Tata Letak Sinkron Wajib, yang berpotensi menjadi bottleneck performa.

Alasan lain untuk tidak memicu perubahan DOM dalam callback tidak ada aktivitas adalah karena dampak waktu perubahan DOM tidak dapat diprediksi, sehingga kita dapat dengan mudah melewati batas waktu yang diberikan browser.

Praktik terbaiknya adalah hanya membuat perubahan DOM di dalam callback requestAnimationFrame, karena 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 akan menggunakan requestIdleCallback untuk melakukan perubahan, tetapi Anda akan menerapkan patch DOM di callback requestAnimationFrame berikutnya, bukan callback tidak ada aktivitas.

Dengan mengingat hal tersebut, 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 akan dipanggil, yang akan menyiapkan satu callback requestAnimationFrame yang pada akhirnya 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 transparan ke setTimeout. API ini ada karena 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 frame, jadi paling baik Anda harus membuat tebakan. API seperti setTimeout, setInterval, atau setImmediate dapat digunakan untuk menjadwalkan pekerjaan, tetapi tidak diberi waktu untuk menghindari interaksi pengguna seperti requestIdleCallback.
  • Apa yang terjadi jika saya melampaui batas waktu? Jika timeRemaining() menampilkan nol, tetapi Anda memilih untuk menjalankannya lebih lama, Anda dapat melakukannya tanpa khawatir browser akan menghentikan pekerjaan Anda. Namun, browser memberi Anda batas waktu untuk mencoba dan memastikan pengalaman yang lancar bagi pengguna, jadi kecuali jika ada alasan yang sangat baik, Anda harus selalu mematuhi batas waktu tersebut.
  • 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 agar tidak lebih dari 100 md. Jika pengguna berinteraksi, jendela 50 md, dalam sebagian besar kasus, akan 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 jenis pekerjaan yang tidak boleh saya lakukan di requestIdleCallback? Idealnya, pekerjaan yang Anda lakukan harus dalam bagian kecil (mikrotugas) 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, gambar, dan komposisi. Dengan demikian, 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. Anda tidak boleh mengharapkan callback dipanggil per frame, dan jika Anda mewajibkannya untuk berjalan dalam jangka waktu tertentu, Anda harus menggunakan waktu tunggu.
  • Dapatkah saya memiliki beberapa callback requestIdleCallback? Ya, Anda dapat melakukannya, sama seperti Anda dapat memiliki beberapa callback requestAnimationFrame. Namun, perlu diingat bahwa jika callback pertama Anda menghabiskan waktu yang tersisa selama callback-nya, tidak akan ada waktu tersisa untuk callback lainnya. Callback lainnya kemudian harus menunggu hingga browser tidak ada aktivitas lagi sebelum dapat dijalankan. Bergantung pada pekerjaan yang ingin Anda selesaikan, mungkin lebih baik menggunakan satu callback nonaktif dan membagi pekerjaan di sana. Atau, Anda dapat menggunakan 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 baru akan dijadwalkan untuk dijalankan sesegera mungkin, mulai dari frame berikutnya (bukan frame saat ini).

Mode tidak ada aktivitas aktif.

requestIdleCallback adalah cara yang bagus untuk memastikan Anda dapat menjalankan kode, tetapi tanpa mengganggu pengguna. Aplikasi ini mudah digunakan dan sangat fleksibel. Namun, ini masih dalam tahap awal, dan spesifikasinya belum sepenuhnya diputuskan, jadi masukan apa pun yang Anda miliki akan kami terima dengan senang hati.

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