Pertanyaan dan kapan menggunakannya
Anda sedang menangani sebuah klik pada dashboard data. Mulailah dengan memprediksi output dari kode ini:
console.log("A");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => {
console.log("promise");
queueMicrotask(() => console.log("nested"));
});
queueMicrotask(() => console.log("microtask"));
console.log("B");Jelaskan setiap langkahnya, lalu analisis masalah performa berikut:
let remaining = 100_000;
function continueInMicrotask() {
remaining -= 1;
if (remaining > 0) {
queueMicrotask(continueInMicrotask);
}
}
queueMicrotask(continueInMicrotask);
setTimeout(() => console.log("timer can run"), 0);
requestAnimationFrame(() => console.log("frame can render"));Jelaskan mengapa timer, klik berikutnya, dan painting tertunda, lalu rancang ulang komputasi batch tersebut agar halaman tetap responsif. Pertanyaan ini berkaitan dengan JavaScript pada main thread browser. Sebuah worker memiliki event loop-nya sendiri, dan Node.js memiliki model fase yang tidak boleh langsung disalin ke dalam jawaban browser.
Ini berguna untuk wawancara tingkat menengah (mid-level) dan senior pada posisi frontend, web-performance, dan full-stack. Jawaban yang kuat menghubungkan model penjadwalan dengan pengalaman pengguna yang dapat diamati: penyelesaian tugas pada akhirnya bukan berarti halaman tetap interaktif selama pekerjaan sedang berjalan.
Apa yang sedang dievaluasi oleh pewawancara
Sinyal pertama adalah model yang akurat. Eksekusi skrip awal, callback klik, dan callback timer yang telah kedaluwarsa berjalan sebagai task. Reaksi Promise, callback queueMicrotask(), dan callback MutationObserver menggunakan microtask. Setelah sebuah task selesai, event loop melakukan microtask checkpoint dan terus memproses microtask sampai antrean kosong.
Sinyal kedua adalah apakah kandidat menghindari penggambaran browser sebagai sistem yang hanya memiliki tepat satu “macrotask queue” permanen. Standar HTML mengizinkan sumber task yang berbeda dikaitkan dengan task queue yang berbeda. Browser dapat membuat pilihan yang ditentukan oleh implementasi (implementation-defined) di antara antrean yang siap dijalankan (runnable) sambil tetap mempertahankan urutan dalam satu sumber task. “Macrotask” bisa menjadi istilah percakapan informal, tetapi task adalah istilah standar yang lebih akurat.
Sinyal ketiga adalah batas rendering yang benar. Hanya setelah microtask checkpoint berakhir, browser dapat berpindah ke task lain atau pembaruan rendering, dan rendering tidak dijamin terjadi setelah setiap task. Jika kode terus menambahkan microtask lain sebelum antrean kosong, event input, timer, dan peluang rendering semuanya dapat mengalami starvation (kelaparan sumber daya).
Sinyal keempat adalah memilih primitif penjadwalan yang tepat. await Promise.resolve() hanya melanjutkan fungsi di dalam microtask, sehingga tidak mengizinkan task atau paint berikutnya berjalan terlebih dahulu. Yield main-thread yang sebenarnya menjadwalkan kelanjutan dalam task di masa mendatang, seperti scheduler.yield() jika didukung atau fallback setTimeout(). Pekerjaan CPU yang tidak dapat dipecah dengan aman seharusnya ditempatkan di dalam worker.
Pertanyaan untuk diklarifikasi sebelum menjawab
- Browser mana saja yang harus didukung?
scheduler.yield()tidak tersedia di setiap browser yang banyak digunakan, sehingga dukungan luas memerlukan deteksi fitur dan fallback. - Bisakah pekerjaan dibagi antar rekaman, atau dapatkah satu panggilan memblokir untuk waktu yang lama? Loop yang dapat dibagi dapat melakukan yield di antara batch. Jika satu rekaman itu sendiri membutuhkan proses yang berat, batching pada main thread tetap memungkinkan pemblokiran yang lama; gunakan worker atau ubah algoritmanya.
- Apakah progres harus dirender setelah setiap batch, atau hanya hasil akhir yang dibutuhkan? Progres visual memerlukan yield main-thread setelah memperbarui state. Hasil yang hanya menampilkan kondisi akhir dapat menghindari biaya DOM, layout, dan paint yang berulang.
- Haruskah hasil di-commit dalam urutan yang ketat? Worker dapat menghitung secara bersamaan (konkuren), tetapi penyelesaian yang tidak berurutan membutuhkan nomor urut, aturan penggabungan (merge rule), atau buffer commit yang terurut.
- Haruskah pemrosesan berlanjut di tab latar belakang?
requestAnimationFrame()dijeda di sebagian besar tab latar belakang, sehingga ini adalah penjadwal umum yang buruk untuk progres latar belakang wajib. - Bagaimana tingkat responsivitas akan diterima? Sepakati latensi interaksi, batas anggaran per-batch, total throughput, dan perilaku pembatalan sehingga kriteria “tidak macet” menjadi dapat diuji.
Kerangka jawaban 30 detik
“Browser memilih satu task yang dapat dijalankan, menjalankannya, lalu melakukan microtask checkpoint yang mengosongkan antrean microtask. Hanya setelah itu browser dapat berpindah ke rendering atau task lain. Cuplikan pertama mencatat A dan B secara sinkron, kemudian promise dan microtask sesuai urutan antrean. Callback promise menambahkan nested di belakang microtask yang sudah menunggu, dan timeout berjalan terakhir. Microtask rekursif membuat checkpoint tetap terbuka, membuat timer, input, dan rendering mengalami starvation. await Promise.resolve() masih merupakan microtask, jadi itu bukan yield main-thread yang sebenarnya. Saya akan membagi waktu (time-slice) pekerjaan yang dapat dipecah dan memanggil scheduler.yield() yang terdeteksi fiturnya di antara batch, dengan setTimeout() sebagai fallback. Jika satu unit masih berat bagi CPU, saya akan memindahkannya ke worker, lalu memverifikasi latensi interaksi dengan rekaman performa dan input nyata.”
Solusi langkah demi langkah
Langkah 1: Tentukan output dari waktu enqueue
Seluruh skrip saat ini berjalan sebagai satu task. Pernyataan sinkron tidak menunggu event loop, sehingga output pertama adalah:
A
BsetTimeout(..., 0) berarti setelah kondisi waktu terpenuhi, callback-nya dapat menjadi task di masa mendatang. Nilai nol tidak menginterupsi skrip saat ini. Promise.resolve().then(...) mengantrekan reaksi Promise sebagai microtask pertama, dan queueMicrotask(...) berikutnya mengantrekan microtask kedua.
Ketika task skrip selesai, microtask checkpoint dimulai. Microtask pertama mencatat log promise dan menambahkan nested ke akhir antrean microtask. Microtask eksplisit sudah menunggu, sehingga mencatat microtask sebelum nested. Setelah antrean microtask kosong, task timer mendapat kesempatan untuk berjalan:
A
B
promise
microtask
nested
timeoutUntuk setiap baris, catat kapan callback dimasukkan ke dalam antrean dan kategori penjadwalan mana yang dimasukinya. “Microtask memiliki prioritas” tidak cukup jika salah satu callback yang bersaing belum dimasukkan ke antrean.
Langkah 2: Tetapkan batasan task, microtask, dan rendering
Urutan yang disederhanakan dan dapat digunakan kembali adalah:
- Browser memilih satu task dari task queue yang siap dijalankan.
- Browser menjalankan task tersebut hingga call stack JavaScript kosong.
- Browser melakukan microtask checkpoint; microtask yang ditambahkan selama checkpoint diproses dalam checkpoint yang sama.
- Berdasarkan peluang rendering, visibilitas dokumen, dan kebijakan implementasi, browser dapat memperbarui rendering.
- Loop berlanjut dan dapat memproses task lain.
Ini menjelaskan dua pengamatan praktis. Pertama, jika sebuah click handler mengubah DOM dan langsung memulai kalkulasi sinkron yang besar, pengguna biasanya tidak melihat status perantara karena browser belum mendapatkan kembali peluang paint. Kedua, microtask cocok untuk pekerjaan konsistensi singkat yang harus terjadi sebelum event dan timer lain, tetapi tidak untuk komputasi rekursif yang besar atau tak terbatas.
Langkah 3: Diagnosis microtask starvation
Setiap pemanggilan microtask pertama pada cuplikan kedua mengantrekan microtask lainnya. Checkpoint tidak dapat selesai hingga antreannya kosong, sehingga 100.000 callback diselesaikan sebelum task timer atau task klik berikutnya dapat berjalan.
Tanpa kondisi penghentian, antrean microtask tidak pernah kosong dalam model penjadwalan. Browser pada akhirnya mungkin menampilkan peringatan halaman tidak responsif, menghentikan halaman, atau menerapkan pengaman implementasi, tetapi kebenaran aplikasi tidak dapat bergantung pada hal tersebut. requestAnimationFrame() tidak melakukan preempt terhadap JavaScript yang sedang berjalan; API ini meminta callback sebelum repaint di masa mendatang. Jika main thread tidak pernah mencapai langkah-langkah rendering yang relevan, callback akan terus menunggu.
Yield yang tampak ini masih salah:
async function processAll(records) {
for (const record of records) {
normalize(record);
await Promise.resolve();
}
}Setiap kelanjutan await dilanjutkan melalui microtask Promise. Call stack sempat kosong sesaat, tetapi checkpoint terus mengonsumsi microtask tersebut, sehingga task input dan timer berikutnya tetap tidak dapat masuk.
Langkah 4: Bagi pekerjaan yang dapat dipecah ke berbagai task
Beri setiap batch anggaran waktu yang terukur, lalu lakukan yield yang sebenarnya di antara batch:
function yieldToMain() {
return globalThis.scheduler?.yield
? globalThis.scheduler.yield()
: new Promise((resolve) => setTimeout(resolve, 0));
}
async function processRecords(records, budgetMs = 5) {
let index = 0;
while (index < records.length) {
const deadline = performance.now() + budgetMs;
while (index < records.length && performance.now() < deadline) {
normalize(records[index]);
index += 1;
}
updateProgress(index / records.length);
if (index < records.length) {
await yieldToMain();
}
}
}Nilai 5 milidetik adalah asumsi awal untuk latihan ini, bukan standar lintas perangkat. Sesuaikan nilainya pada perangkat target berdasarkan biaya per-item, latensi interaksi, dan throughput. scheduler.yield() menjadwalkan kelanjutan sebagai task prioritas berikutnya, memberi browser kesempatan untuk menangani pekerjaan yang diperlukan terlebih dahulu. Karena dukungan browser-nya belum lengkap, contoh ini mendeteksi fiturnya. Fallback setTimeout() memiliki kompatibilitas lebih luas tetapi dipengaruhi oleh clamping timer, kebijakan latar belakang, dan persaingan dari task lain.
Ada batasan lain: anggaran hanya dapat diperiksa di antara pemanggilan ke normalize(). Jika satu pemanggilan memblokir selama 80 milidetik, anggaran lima milidetik tidak dapat membantu. Pecah normalize(), ganti algoritmanya, atau pindahkan komputasi ke worker.
Langkah 5: Cocokkan primitif dengan pekerjaannya
| Kebutuhan | Pilihan | Biaya utama atau batasan |
|---|---|---|
| Menjalankan pembersihan singkat atau notifikasi konsistensi setelah task saat ini tetapi sebelum event lain | queueMicrotask() | Rekursi atau komputasi berat menyebabkan starvation pada pekerjaan lain |
| Membagi pekerjaan main-thread yang panjang sambil mempertahankan kelanjutan yang diprioritaskan | scheduler.yield() | Memerlukan deteksi fitur; dukungan belum lengkap |
| Memindahkan kelanjutan ke task mendatang dengan kompatibilitas luas | setTimeout() | Penundaan timer dan penjadwalan tidak deterministik |
| Memperbarui status animasi sebelum repaint mendatang | requestAnimationFrame() | Pekerjaan callback yang berat tetap memblokir paint tersebut; tab latar belakang umumnya menjedanya |
| Menjalankan pekerjaan berat CPU yang tidak dapat dipecah secara aman | Web Worker | Biaya protokol pesan, penyalinan data, atau shared-memory |
requestAnimationFrame() menyelaraskan pekerjaan visual dengan painting; ini bukan antrean background-job umum. Gunakan untuk mengirimkan perubahan visual yang ringan, bukan untuk menyembunyikan kalkulasi besar tepat sebelum paint. Sebuah worker memindahkan komputasi CPU dari main thread halaman, tetapi tidak secara otomatis menyelesaikan pembatalan, pelaporan progres, pengurutan hasil, atau biaya transfer. Hal-hal tersebut memerlukan protokol eksplisit.
Langkah 6: Verifikasi responsivitas, bukan hanya penyelesaian
Verifikasi harus mencakup setidaknya empat lapisan:
- Uji urutan: Catat kode sinkron, reaksi Promise,
queueMicrotask(), dan timer di halaman minimal dan konfirmasikan bahwa urutan yang diamati cocok dengan hasil penurunan logika. - Inspeksi timeline: Rekam klik, batch, dan painting progres di browser performance tools. Periksa long task, microtask yang terus-menerus, jeda frame, dan kapan callback input benar-benar berjalan.
- Uji beban (stress) dan pembatalan: Tingkatkan jumlah rekaman, batasi (throttle) CPU, lakukan klik dan scroll selama pemrosesan, dan batalkan operasi. Verifikasi bahwa antrean tidak tumbuh tanpa batas.
- Lingkungan batas: Verifikasi fallback di browser tanpa
scheduler.yield(). Pindahkan halaman ke tab latar belakang dan konfirmasikan alur kerja bisnis tidak secara keliru bergantung pada callbackrequestAnimationFrame()yang terus-menerus.
Penerimaan membutuhkan waktu penyelesaian dan latensi interaksi. Batching menambah overhead penjadwalan dan mungkin sedikit meningkatkan total durasi; tujuannya adalah memberikan ruang eksekusi untuk input, painting, dan task penting lainnya. Jika biaya throughput tersebut tidak dapat diterima, optimalkan algoritma atau gunakan worker daripada mengisi kembali main thread dengan microtask.
Contoh jawaban yang kuat
“Saya akan menentukan urutannya dari waktu enqueue. Skrip saat ini adalah satu task, jadi A dan B bersifat sinkron. Timer hanya menjadwalkan task di masa mendatang. Reaksi Promise masuk ke microtask queue sebelum callback queueMicrotask eksplisit, jadi checkpoint mencatat promise terlebih dahulu. Callback tersebut menambahkan nested di belakang microtask yang sudah menunggu. Urutan akhirnya adalah A, B, promise, microtask, nested, timeout.
Setelah sebuah task, browser melakukan microtask checkpoint dan terus memproses hingga antrean microtask kosong. Cuplikan kedua terus mengisi kembali antrean tersebut, sehingga checkpoint tetap terbuka untuk waktu yang lama. Timer dan klik adalah task berikutnya, dan painting membutuhkan main thread untuk mencapai peluang rendering, sehingga semuanya tertunda. requestAnimationFrame tidak dapat menginterupsi JavaScript, dan await Promise.resolve hanya melanjutkan di microtask lain, jadi keduanya tidak memperbaiki starvation tersebut.
Jika setiap rekaman diproses dengan cepat, saya akan memprosesnya berdasarkan anggaran waktu dan memanggil scheduler.yield di antara batch. Karena API ini tidak tersedia di setiap browser, saya akan mendeteksi fiturnya dan menyiapkan fallback ke setTimeout. Memulai dengan anggaran lima milidetik hanyalah sebuah eksperimen; saya akan menyesuaikannya pada perangkat target berdasarkan latensi input dan throughput. Jika satu rekaman membutuhkan proses yang berat, saya akan memecah operasi tersebut atau memindahkannya ke worker.
Terakhir, saya akan merekam linimasa performa dan memastikan tidak ada waterfall microtask yang terus-menerus, progres benar-benar ter-paint, klik dan scrolling berjalan selama pemrosesan, serta pembatalan, tab latar belakang, dan fallback kompatibilitas berfungsi dengan benar. Desain ini menerima sedikit overhead penjadwalan sebagai ganti responsivitas yang terukur.”
Kesalahan umum
- Menghafal “sync, microtask, macrotask” → Cara ini tidak dapat menjelaskan mengapa microtask bersarang berjalan di belakang microtask yang sudah diantrekan dan mengabaikan adanya beberapa sumber task → Beri anotasi waktu enqueue, kategori, dan status antrean baris demi baris.
- Menyebut
setTimeout(..., 0)bersifat langsung (immediate) → Timer yang kedaluwarsa hanya membuat task di masa mendatang memenuhi syarat (eligible) dan tidak dapat menginterupsi task atau checkpoint saat ini → Nyatakan waktu paling awal yang memenuhi syarat tanpa menjanjikan waktu yang tepat. - Mengasumsikan painting terjadi setelah setiap microtask → Checkpoint menguras microtask secara terus-menerus, dan peluang rendering setelahnya mungkin tetap dilewati → Bahas painting setelah checkpoint selesai sepenuhnya.
- Memecah proses (chunking) dengan
await Promise.resolve()→ Kelanjutannya masih berupa microtask dan tidak memberikan ruang bagi task input atau timer berikutnya → Jadwalkan kelanjutan dalam task di masa mendatang. - Menggunakan pemanggilan
queueMicrotask()tanpa henti untuk “prioritas” → Hal ini menyebabkan starvation pada task lain dan rendering → Khususkan microtask untuk pekerjaan konsistensi yang singkat dan terbatas. - Memindahkan pekerjaan berat ke dalam
requestAnimationFrame()→ Callback masih berjalan di main thread sebelum paint, sehingga pekerjaan berat akan menunda frame → Pertahankan pekerjaan visual rAF tetap ringan dan lakukan chunking atau offload pekerjaan CPU. - Memanggil
scheduler.yield()tanpa syarat → Beberapa browser yang banyak digunakan tidak mendukungnya → Lakukan deteksi fitur dan uji fallbacksetTimeout(). - Hanya mengukur total durasi → Throughput normal dapat menyembunyikan periode panjang di mana input tidak responsif → Periksa juga latensi interaksi, long task, frame, dan pertumbuhan antrean.
- Menggunakan satu ukuran batch yang sama di mana-mana → Biaya per-item, refresh rate, dan kecepatan perangkat bervariasi → Mulailah dengan anggaran waktu dan sesuaikan di lingkungan target.
Pertanyaan lanjutan dan jawabannya
Lanjutan 1: Mana yang berjalan lebih dulu, dua timer tanpa penundaan (zero-delay) atau sebuah klik?
Nama API saja tidak dapat menentukan urutan universal di berbagai sumber task. Browser dapat mengelola beberapa task queue dan memilih di antara antrean yang siap dijalankan sambil tetap menjaga urutan dalam satu sumber task. Klarifikasi kapan timer menjadi eligible, kapan klik terjadi, dan apa lagi yang sedang membebani halaman, lalu amati implementasi targetnya. Cuplikan wawancara yang deterministik biasanya mengontrol kondisi-kondisi ini.
Lanjutan 2: Jika scheduler.yield() mengembalikan Promise, mengapa itu disebut yield yang sebenarnya?
Karakteristik pentingnya adalah bagaimana API menjadwalkan resolusi (penyelesaian), bukan tipe pengembaliannya. Kelanjutan setelah Promise yang sudah selesai akan bergabung dengan microtask checkpoint saat ini. scheduler.yield() menjadwalkan task prioritas berikutnya yang menyelesaikan Promise-nya, memungkinkan pekerjaan yang tertunda seperti input berjalan sebelum kelanjutan tersebut. API ini tetap membutuhkan deteksi fitur, dan operasi-operasi kecil tidak boleh masing-masing dijadikan task terpisah.
Lanjutan 3: Bisakah pemrosesan 100.000 rekaman di dalam worker tetap membuat halaman macet (freeze)?
Bisa. Main thread masih dapat kelebihan beban akibat pesan yang berlebihan, penyalinan data yang besar, perubahan DOM yang terlalu sering, atau penggabungan hasil yang memakan banyak resource. Lakukan batching pada pesan progres, batasi frekuensinya, evaluasi objek yang dapat ditransfer (transferable objects) atau protokol shared-memory yang tepat, dan commit perubahan visual yang diperlukan saja pada main thread.
Lanjutan 4: Kapan queueMicrotask() harus digunakan?
Gunakan untuk pekerjaan singkat dan terbatas yang harus berjalan setelah logika sinkron saat ini tetapi sebelum event lain, seperti memberikan urutan callback yang sama pada hit cache sinkron dan miss berbasis Promise, atau melakukan batching pada satu notifikasi internal library. Ini bukan penjadwal untuk pekerjaan panjang. Sebutkan aturan penghentian dan batasan beban pekerjaannya.
Lanjutan 5: Bagaimana Anda membatalkan operasi yang dipecah menjadi beberapa bagian (chunked)?
Periksa AbortSignal di awal setiap batch dan setelah setiap yield, hentikan pengambilan rekaman, dan cegah operasi lama menimpa progres atau hasil dari permintaan yang lebih baru. Desain worker memerlukan pesan pembatalan dan aturan untuk membuang hasil yang datang terlambat. Pemeriksaan yang terlalu jarang akan lambat bereaksi; pemeriksaan di sekitar setiap operasi kecil menambah overhead, jadi lakukan pengukuran di batas-batas batch.
Lanjutan 6: Apakah requestAnimationFrame() menjamin paint langsung terjadi?
Tidak. API ini meminta callback sebelum repaint di masa mendatang, sementara browser dapat menjadwalkan atau melewati rendering berdasarkan visibilitas dan kebijakan rendering. Sebagian besar browser menjeda callback ini di tab latar belakang. Bahkan ketika callback berjalan, pekerjaan panjang di dalamnya akan terus menunda proses paint yang sebenarnya.