Menggunakan requestIdleCallback

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

Menggunakan requestIdleCallback untuk menjadwalkan pekerjaan yang tidak penting.

Kabar baiknya, kini ada API yang dapat membantu: requestIdleCallback. Dengan cara yang sama ketika mengadopsi requestAnimationFrame memungkinkan kita untuk menjadwalkan animasi dengan benar dan memaksimalkan peluang untuk mencapai 60 fps, requestIdleCallback akan menjadwalkan pekerjaan ketika ada waktu luang di akhir frame, atau saat pengguna tidak aktif. Ini berarti ada kesempatan untuk melakukan pekerjaan Anda tanpa menghalangi pengguna. Fitur ini tersedia mulai Chrome 47, jadi Anda dapat mencobanya kembali sekarang dengan menggunakan Chrome Canary. Ini adalah fitur eksperimental, dan spesifikasinya masih berubah-ubah, sehingga segala sesuatunya 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 lain yang perlu dijalankan. Solusi home-roll tidak dapat memperhitungkan solusi tersebut. Untuk memastikan bahwa pengguna tidak berinteraksi dengan cara tertentu, Anda juga perlu menambahkan pemroses ke setiap jenis peristiwa interaksi (scroll, touch, click), meskipun Anda tidak memerlukannya untuk fungsi, hanya agar Anda dapat benar-benar yakin bahwa pengguna tersebut 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 bahas lebih lanjut dan melihat bagaimana kita dapat memanfaatkannya.

Memeriksa requestIdleCallback

requestIdleCallback masih dalam tahap awal. Jadi, sebelum menggunakannya, sebaiknya periksa apakah perangkat tersedia untuk digunakan:

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

Anda juga dapat melakukan shim perilakunya, yang perlu melakukan fallback 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 kode tersebut tidak mengetahui waktu tidak ada aktivitas seperti requestIdleCallback. Namun, karena Anda akan memanggil fungsi secara langsung jika requestIdleCallback tidak tersedia, Anda dapat melakukan shiming dengan cara ini. Dengan shim, jika requestIdleCallback tersedia, panggilan Anda akan dialihkan secara diam-diam, dan ini bagus.

Untuk saat ini, mari asumsikan bahwa hal itu ada.

Menggunakan requestIdleCallback

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

requestIdleCallback(myNonEssentialWork);

Saat myNonEssentialWork dipanggil, Anda akan diberi objek deadline yang berisi fungsi yang menampilkan angka yang menunjukkan sisa waktu 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 lain jika masih ada tugas lain yang harus dilakukan:

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

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

Memastikan bahwa {i>function<i} Anda disebut

Apa yang Anda lakukan jika sedang sangat sibuk? Anda mungkin khawatir bahwa callback mungkin tidak pernah dipanggil. Ya, meskipun requestIdleCallback mirip dengan requestAnimationFrame, metode ini juga berbeda karena menggunakan parameter kedua opsional: objek opsi dengan properti waktu tunggu. Jika disetel, waktu tunggu ini akan memberi browser waktu dalam milidetik untuk mengeksekusi callback:

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

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

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

Jika Anda melihat bahwa didTimeout 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 adanya potensi gangguan, waktu tunggu ini dapat menyebabkan pengguna Anda (pekerjaan dapat menyebabkan aplikasi menjadi tidak responsif atau tersendat) berhati-hatilah dalam menyetel parameter ini. Jika memungkinkan, biarkan browser memutuskan kapan harus memanggil callback.

Menggunakan requestIdleCallback untuk mengirim data analisis

Mari kita lihat penggunaan requestIdleCallback untuk mengirim data analisis. Dalam hal ini, kita mungkin ingin melacak suatu peristiwa seperti -- katakanlah -- mengetuk menu navigasi. Namun, karena peristiwa ini biasanya bergerak ke layar, kita sebaiknya tidak langsung mengirim peristiwa ini ke Google Analytics. Kita akan membuat array peristiwa untuk dikirim dan memintanya agar dikirim 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 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 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 waktu tertentu di masa mendatang.

Terakhir, kita harus 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 berasumsi bahwa jika requestIdleCallback tidak ada, maka data analisis harus segera dikirim. Namun, dalam aplikasi produksi, akan lebih baik menunda pengiriman dengan waktu tunggu untuk memastikan bahwa tidak terjadi konflik 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, seperti menambahkan item ke akhir daftar yang terus bertambah dan lambat dimuat. Mari kita lihat bagaimana requestIdleCallback sebenarnya sesuai dengan bingkai biasa.

Frame biasa.

Ada kemungkinan bahwa browser akan terlalu sibuk untuk menjalankan callback dalam frame tertentu, sehingga Anda tidak perlu mengharapkan bahwa akan ada waktu luang di akhir frame untuk melakukan pekerjaan lagi. Hal itu membuatnya berbeda dengan sesuatu seperti setImmediate, yang berjalan per frame.

Jika callback diaktifkan di akhir frame, callback akan dijadwalkan untuk berjalan 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 yang 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 Forced Synchronous Layout, yang merupakan potensi bottleneck performa.

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

Praktik terbaiknya adalah hanya membuat perubahan DOM di dalam callback requestAnimationFrame, karena ini 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 membuat perubahan, tetapi Anda akan 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 akan dipanggil, yang akan menyiapkan callback requestAnimationFrame tunggal 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;
}

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

FAQ

  • Apakah ada polyfill? Sayangnya tidak, tetapi ada shim jika Anda ingin melakukan pengalihan transparan ke setTimeout. Alasan dibuatnya API ini adalah karena membuka kesenjangan yang sangat nyata di platform web. Menyimpulkan kurangnya aktivitas sulit, tetapi tidak ada JavaScript API untuk menentukan jumlah waktu luang di akhir frame, jadi yang terbaik adalah Anda harus menebak-nebak. API seperti setTimeout, setInterval, atau setImmediate dapat digunakan untuk menjadwalkan pekerjaan, tetapi tidak dibatasi waktu untuk menghindari interaksi pengguna sebagaimana requestIdleCallback.
  • Apa yang terjadi jika saya melewati batas waktu? Jika timeRemaining() menampilkan nol, namun Anda memilih untuk berjalan lebih lama, Anda dapat melakukannya tanpa takut browser menghentikan pekerjaan Anda. Namun, browser memberi Anda tenggat waktu untuk mencoba dan memastikan pengalaman yang lancar bagi pengguna, jadi kecuali ada alasan yang kuat, Anda harus selalu mematuhi tenggat 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 disimpan di bawah 100 md. Jika pengguna berinteraksi dalam periode 50 md, pada sebagian besar kasus, callback tidak ada aktivitas dapat diselesaikan, dan agar browser merespons interaksi pengguna. Anda mungkin akan 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 dalam requestIdleCallback? Idealnya, pekerjaan yang Anda lakukan harus berupa potongan-potongan kecil (microtasks) 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. 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 langsung dieksekusi 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 saat pengguna tidak aktif. Jangan berharap callback akan dipanggil per frame, dan jika Anda mengharuskan callback untuk berjalan dalam jangka waktu yang ditentukan, sebaiknya gunakan waktu tunggu.
  • Dapatkah saya memiliki beberapa callback requestIdleCallback? Ya, tentu saja Anda bisa, sebanyak yang Anda bisa lakukan adalah memiliki beberapa callback requestAnimationFrame. Namun, perlu diingat bahwa jika callback pertama Anda menggunakan waktu yang tersisa selama callback, maka tidak akan ada lagi waktu untuk callback lainnya. Callback lainnya kemudian harus menunggu hingga browser tidak ada aktivitas berikutnya sebelum dapat dijalankan. Bergantung pada pekerjaan yang ingin Anda selesaikan, mungkin lebih baik memiliki satu callback tidak ada aktivitas dan membagi pekerjaan di sana. Atau, Anda dapat menggunakan waktu tunggu untuk memastikan bahwa 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 frame saat ini).

Tidak ada aktivitas aktif!

requestIdleCallback adalah cara yang tepat untuk memastikan Anda dapat menjalankan kode, tetapi tanpa mengganggu pengguna. Alat ini mudah digunakan dan sangat fleksibel. Ini masih awal, dan spesifikasi belum sepenuhnya ditetapkan, jadi masukan apa pun yang Anda terima sangat diharapkan.

Cobalah di Chrome Canary, cobalah untuk project Anda, dan beri tahu kami cara Anda melakukannya.