Learn Javascript Frontend Advanced Part 004 Event Loop Tasks Microtasks And Rendering
title: Learn Advanced JavaScript for Web / Frontend Engineering - Part 004 description: "Deep dive ke browser event loop: tasks, microtasks, Promise jobs, rendering opportunity, requestAnimationFrame, timers, input responsiveness, starvation, scheduling, cancellation, dan debugging async UI bugs." series: learn-javascript-frontend-advanced seriesTitle: Learn Advanced JavaScript for Web / Frontend Engineering order: 4 partTitle: Event Loop, Tasks, Microtasks, and Rendering tags:
- javascript
- frontend
- web
- event-loop
- async
- rendering
- browser
- performance date: 2026-06-27
Event Loop, Tasks, Microtasks, and Rendering
Part ini menjawab pertanyaan inti yang sering membedakan frontend engineer biasa dan frontend engineer kuat:
Kapan sebuah callback benar-benar berjalan, dan apa dampaknya terhadap rendering, input responsiveness, race condition, dan user experience?
Event loop bukan sekadar interview topic. Ia adalah fondasi untuk:
- UI freeze;
- delayed click handling;
- Promise ordering;
setTimeout(..., 0)yang tidak langsung jalan;- microtask starvation;
- animation jank;
- hydration scheduling;
- debounced search;
- retry/backoff;
- cancellation;
- long task;
- race condition pada data fetching;
- state update timing dalam framework.
Part sebelumnya membahas semantic JavaScript. Part ini membahas bagaimana host browser mengatur kapan JavaScript diberi kesempatan berjalan dan kapan browser melakukan rendering.
1. Posisi Dalam Framework Kaufman
Kaufman menyarankan memecah skill menjadi sub-skill yang bisa dilatih. Event loop adalah sub-skill kecil tetapi berdampak luas.
Target part ini: Anda bisa melihat bug asynchronous dan memetakan bug itu ke queue, phase, dan rendering opportunity yang benar.
2. Batas Konsep: ECMAScript vs Browser Host
ECMAScript mendefinisikan bahasa dan Promise jobs. Browser mendefinisikan event loop, task source, timers, DOM events, rendering update, dan integration dengan APIs.
Jadi, kalimat "JavaScript event loop" sebenarnya ringkas tapi kurang presisi. Lebih tepat:
- ECMAScript punya jobs dan agents;
- browser HTML standard mendefinisikan event loop processing model;
- Web APIs mengantrikan tasks atau callbacks ke mekanisme host;
- rendering engine punya kesempatan update rendering di antara pekerjaan tertentu;
- browser implementation punya optimisasi dan kebijakan scheduling sendiri.
Untuk engineering, pemisahan ini penting. Promise ordering tidak sama dengan timer ordering. requestAnimationFrame bukan "microtask". Rendering bukan callback JavaScript biasa.
3. Main Thread Sebagai Resource Terbatas
Di browser, banyak pekerjaan UI terjadi di main thread:
- menjalankan JavaScript;
- memproses event handler;
- mengubah DOM;
- style calculation;
- layout;
- paint preparation;
- sebagian rendering coordination;
- menjalankan framework reconciliation;
- menjalankan synchronous storage APIs;
- parsing HTML/CSS pada konteks tertentu.
Browser modern punya banyak thread internal, tetapi JavaScript UI application sering bottleneck di main thread.
Jika JavaScript menjalankan pekerjaan panjang, browser tidak bisa merespons input dan tidak bisa update visual tepat waktu.
4. Event Loop Mental Model
Sederhanakan dulu:
Model ini sengaja disederhanakan, tetapi cukup untuk mayoritas debugging frontend.
Rule utama:
- satu task berjalan sampai selesai;
- setelah task selesai, browser menjalankan microtasks sampai queue kosong;
- setelah itu browser punya kesempatan melakukan rendering update;
- jika task atau microtask terlalu lama, rendering dan input tertunda.
5. Run-to-Completion
JavaScript task berjalan sampai selesai. Browser tidak menghentikan function Anda di tengah kecuali hal luar biasa seperti page termination.
button.addEventListener("click", () => {
expensiveStep1();
expensiveStep2();
expensiveStep3();
});
Selama handler ini berjalan:
- click berikutnya tidak diproses;
- paint tidak terjadi;
- Promise callback lain tidak berjalan;
- timer callback lain menunggu;
- user bisa melihat UI freeze.
5.1 Run-to-Completion Menghindari Data Race Sederhana
Kelebihan model ini: tidak ada thread lain yang tiba-tiba mengubah variable JavaScript Anda di tengah synchronous function.
let count = 0;
button.addEventListener("click", () => {
const before = count;
count = before + 1;
});
Namun race tetap bisa terjadi antar async boundary.
let version = 0;
async function refresh() {
const localVersion = ++version;
const data = await fetchData();
if (localVersion === version) {
render(data);
}
}
Async boundary membuka kemungkinan hasil lama datang setelah hasil baru.
6. Task
Task adalah unit kerja yang dimasukkan host ke event loop.
Sumber task dapat mencakup:
- script initial execution;
- timer callback;
- DOM event;
- network event;
- message event;
- history/navigation event;
- user interaction;
- postMessage;
- MessageChannel.
Contoh:
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
Output:
A
C
B
Timer callback adalah task masa depan. Ia tidak interrupt task saat ini.
6.1 setTimeout(..., 0) Bukan "Sekarang"
0 berarti minimum delay yang diminta, bukan guarantee langsung jalan. Callback harus menunggu:
- current task selesai;
- microtask checkpoint selesai;
- browser scheduling;
- timer clamping;
- tab throttling;
- task lain yang lebih dulu.
setTimeout(() => {
console.log("timer");
}, 0);
queueMicrotask(() => {
console.log("microtask");
});
console.log("sync");
Output umum:
sync
microtask
timer
7. Microtask
Microtask berjalan setelah current task selesai dan sebelum browser kembali mengambil task berikutnya. Promise reactions dan queueMicrotask masuk ke microtask queue.
console.log("A");
Promise.resolve().then(() => {
console.log("B");
});
console.log("C");
Output:
A
C
B
7.1 Microtask Checkpoint
Setelah task selesai, browser menjalankan microtasks sampai queue kosong.
queueMicrotask(() => {
console.log("microtask 1");
queueMicrotask(() => {
console.log("microtask 2");
});
});
console.log("sync");
Output:
sync
microtask 1
microtask 2
Microtask yang menambahkan microtask baru akan ikut dijalankan sebelum browser lanjut ke rendering/task berikutnya.
7.2 Microtask Starvation
Ini berbahaya:
function loop() {
queueMicrotask(loop);
}
loop();
Microtask queue tidak pernah kosong. Browser tidak punya kesempatan rendering atau mengambil task input berikutnya. Page bisa freeze.
Promise juga bisa membuat starvation:
function loop() {
Promise.resolve().then(loop);
}
loop();
8. Promise Ordering
Promise reaction callback adalah microtask.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve()
.then(() => console.log("3"))
.then(() => console.log("4"));
console.log("5");
Output umum:
1
5
3
4
2
Mengapa?
- synchronous code berjalan dulu;
- timer mengantrikan task;
- promise
.thenmengantrikan microtask; - setelah current task selesai, microtasks berjalan;
- baru task timer diambil.
8.1 Promise Callback Tidak Menjadikan CPU Work Async
Promise.resolve().then(() => {
heavyComputation();
});
Ini menunda heavy computation ke microtask, tetapi tetap berjalan di main thread dan tetap bisa block rendering.
Jika pekerjaan berat, Anda butuh chunking, worker, atau algorithmic optimization.
9. async / await Dari Perspektif Scheduling
await memecah function menjadi beberapa bagian. Bagian setelah await berjalan sebagai continuation ketika promise settled.
async function run() {
console.log("A");
await null;
console.log("B");
}
run();
console.log("C");
Output:
A
C
B
Karena await null diperlakukan seperti await promise yang already fulfilled; continuation tetap asynchronous.
9.1 await Bisa Membuka Race
let currentRequestId = 0;
async function loadUser(userId) {
const requestId = ++currentRequestId;
const user = await fetchUser(userId);
if (requestId !== currentRequestId) {
return;
}
renderUser(user);
}
Tanpa guard, response request lama bisa menimpa UI request baru.
10. Rendering Opportunity
Browser tidak render setelah setiap line JavaScript. Browser punya kesempatan update rendering di antara event loop turns dan berdasarkan kebijakan internal.
Contoh:
element.textContent = "Loading...";
heavyComputation();
element.textContent = "Done";
User mungkin tidak pernah melihat "Loading..." karena JavaScript task belum selesai, sehingga browser belum sempat paint.
10.1 Yield Agar Browser Bisa Paint
element.textContent = "Loading...";
setTimeout(() => {
heavyComputation();
element.textContent = "Done";
}, 0);
Ini memberi browser kesempatan untuk memproses rendering sebelum task berikutnya, walau tidak selalu guarantee dalam semua kondisi.
Alternatif modern bergantung support target:
await new Promise((resolve) => requestAnimationFrame(resolve));
Jika ingin memberi kesempatan frame berikutnya:
await new Promise((resolve) => requestAnimationFrame(() => {
requestAnimationFrame(resolve);
}));
Double rAF kadang dipakai untuk memastikan perubahan style awal sudah ter-render sebelum transisi berikutnya, tetapi gunakan dengan hati-hati dan test di target browser.
11. requestAnimationFrame
requestAnimationFrame meminta browser menjalankan callback sebelum repaint berikutnya.
function animate() {
updateModel();
renderFrame();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Use case:
- animation;
- DOM read/write scheduling;
- visual updates;
- measuring frame-to-frame changes;
- coordinating with browser paint cycle.
11.1 rAF Bukan Timer Umum
Jangan gunakan rAF untuk retry network, debounce search, atau polling domain data.
rAF cocok untuk pekerjaan visual. Timer atau scheduler lain lebih cocok untuk non-visual tasks.
11.2 rAF dan Tab Background
Browser bisa pause atau throttle rAF pada background tab. Ini baik untuk battery dan performance.
Jadi jangan taruh domain-critical background work hanya di rAF.
12. requestIdleCallback
requestIdleCallback menjalankan callback saat browser idle. Namun support dan behavior bisa berbeda antar browser, dan tidak cocok untuk pekerjaan yang wajib segera selesai.
Use case:
- non-critical analytics preparation;
- cache warmup ringan;
- precompute optional;
- cleanup ringan;
- background indexing kecil.
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && hasWork()) {
doSmallUnitOfWork();
}
});
Fallback perlu disediakan jika target browser tidak mendukung.
const scheduleIdle =
window.requestIdleCallback ??
((callback) => setTimeout(() => callback({
timeRemaining: () => 0,
didTimeout: true,
}), 1));
Production Rule
Idle callback bukan tempat untuk pekerjaan correctness-critical.
13. Timer Clamping dan Throttling
Timer tidak selalu presisi. Browser bisa melakukan:
- minimum timeout clamping;
- nested timer clamping;
- background tab throttling;
- battery-saving behavior;
- privacy-driven timer precision reduction.
Jangan membuat logic domain yang bergantung pada presisi setTimeout.
Buruk:
setTimeout(() => {
assumePaymentExpired();
}, 15 * 60 * 1000);
Lebih baik:
function isPaymentExpired(payment) {
return Date.now() >= payment.expiresAt;
}
Timer boleh memicu check, tetapi source of truth harus timestamp/domain state.
14. MessageChannel Untuk Yielding
MessageChannel bisa dipakai untuk schedule task dengan overhead rendah.
function createTaskScheduler() {
const queue = [];
const channel = new MessageChannel();
channel.port1.onmessage = () => {
const task = queue.shift();
if (task) {
task();
}
if (queue.length > 0) {
channel.port2.postMessage(null);
}
};
return function schedule(task) {
queue.push(task);
if (queue.length === 1) {
channel.port2.postMessage(null);
}
};
}
const scheduleTask = createTaskScheduler();
scheduleTask(() => {
console.log("scheduled task");
});
Ini berguna untuk chunking pekerjaan tanpa bergantung hanya pada timer.
15. Chunking CPU Work
Masalah:
function processAll(items) {
for (const item of items) {
expensiveProcess(item);
}
}
Jika items besar, UI freeze.
Solusi: pecah menjadi chunk.
async function processInChunks(items, chunkSize = 100) {
for (let index = 0; index < items.length; index += chunkSize) {
const chunk = items.slice(index, index + chunkSize);
for (const item of chunk) {
expensiveProcess(item);
}
await yieldToMain();
}
}
function yieldToMain() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
15.1 Time Budget Lebih Baik Dari Fixed Chunk
async function processWithBudget(items, budgetMs = 8) {
let index = 0;
while (index < items.length) {
const start = performance.now();
while (index < items.length && performance.now() - start < budgetMs) {
expensiveProcess(items[index]);
index += 1;
}
await yieldToMain();
}
}
Kenapa 8ms? Pada layar 60Hz, satu frame sekitar 16.67ms. Jika JavaScript mengambil seluruh budget, style/layout/paint/input tidak punya ruang. Nilai nyata perlu disesuaikan dengan target device.
16. Long Tasks
Long task umumnya berarti pekerjaan main thread yang berlangsung lama dan mengganggu responsiveness. Dalam performance tooling, threshold 50ms sering digunakan untuk menandai task yang cukup panjang untuk mengganggu interaksi.
Dampaknya:
- input terlambat diproses;
- animation frame drop;
- INP memburuk;
- perceived performance turun;
- user menganggap aplikasi hang.
16.1 Long Task Sources
- JSON parsing besar;
- rendering list besar;
- synchronous validation kompleks;
- syntax highlighting besar;
- markdown rendering besar;
- crypto/compression di main thread;
- expensive selector/reducer;
- uncontrolled re-render;
- synchronous localStorage access berulang;
- layout thrashing;
- hydration bundle besar.
17. Input Responsiveness
User interaction adalah pekerjaan time-sensitive.
Buruk:
input.addEventListener("input", (event) => {
const result = expensiveSearch(event.target.value);
render(result);
});
Lebih baik:
input.addEventListener("input", (event) => {
const query = event.target.value;
scheduleSearch(query);
});
Dengan debounce:
function debounce(fn, delay) {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn(...args);
}, delay);
};
}
const scheduleSearch = debounce((query) => {
render(expensiveSearch(query));
}, 150);
Tetapi debounce bukan solusi untuk CPU work yang tetap berat. Jika expensive search lokal besar, gunakan worker atau indexing.
18. Debounce, Throttle, dan Frame Throttle
18.1 Debounce
Menunggu input berhenti.
Cocok untuk:
- search request;
- autosave;
- validation mahal;
- resize final calculation.
const debouncedSave = debounce(saveDraft, 500);
18.2 Throttle
Membatasi frekuensi.
Cocok untuk:
- scroll tracking;
- pointer move;
- repeated measurement;
- telemetry.
function throttle(fn, interval) {
let last = 0;
let trailingArgs = null;
let timerId = null;
return (...args) => {
const now = Date.now();
const remaining = interval - (now - last);
if (remaining <= 0) {
last = now;
fn(...args);
return;
}
trailingArgs = args;
if (!timerId) {
timerId = setTimeout(() => {
last = Date.now();
timerId = null;
fn(...trailingArgs);
trailingArgs = null;
}, remaining);
}
};
}
18.3 rAF Throttle
Untuk visual event seperti scroll/pointer:
function rafThrottle(fn) {
let scheduled = false;
let latestArgs;
return (...args) => {
latestArgs = args;
if (scheduled) {
return;
}
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
fn(...latestArgs);
});
};
}
Ini menyelaraskan update dengan frame.
19. DOM Read/Write Scheduling
Masalah layout thrashing:
for (const item of items) {
const height = item.element.offsetHeight; // read layout
item.element.style.height = `${height + 10}px`; // write
}
Mencampur read/write bisa memaksa layout berulang.
Lebih baik batch read lalu write:
const heights = items.map((item) => item.element.offsetHeight);
items.forEach((item, index) => {
item.element.style.height = `${heights[index] + 10}px`;
});
Atau schedule:
requestAnimationFrame(() => {
const rect = element.getBoundingClientRect();
requestAnimationFrame(() => {
element.style.transform = `translateX(${rect.width}px)`;
});
});
Detail rendering pipeline dibahas lebih dalam di Part 009.
20. Race Condition Pada Data Fetching
Event loop membuat JavaScript single-threaded per agent, tetapi async I/O tetap bisa menghasilkan race.
let currentUserId;
async function selectUser(userId) {
currentUserId = userId;
const user = await fetchUser(userId);
renderUser(user);
}
Jika user memilih A lalu B, response A bisa datang setelah B dan menimpa UI.
20.1 Version Guard
let requestVersion = 0;
async function selectUser(userId) {
const version = ++requestVersion;
const user = await fetchUser(userId);
if (version !== requestVersion) {
return;
}
renderUser(user);
}
20.2 AbortController
let currentController;
async function selectUser(userId) {
currentController?.abort();
const controller = new AbortController();
currentController = controller;
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
const user = await response.json();
renderUser(user);
} catch (error) {
if (error.name === "AbortError") {
return;
}
showError(error);
}
}
Cancellation akan dibahas lebih dalam di Part 005.
21. Async State Machine
Async flow sebaiknya tidak hanya "loading boolean".
Buruk untuk kasus kompleks:
let loading = false;
let error = null;
let data = null;
Lebih baik model state eksplisit:
const LoadState = {
IDLE: "idle",
LOADING: "loading",
SUCCESS: "success",
ERROR: "error",
};
let state = {
status: LoadState.IDLE,
data: null,
error: null,
};
State transition:
Event loop tidak menghapus kebutuhan state machine. Justru async boundary membuat state machine makin penting.
22. Microtask Dalam Framework
Framework modern sering menggunakan microtask atau scheduler internal untuk batching.
Contoh conceptual:
let pending = false;
function setState(partial) {
applyPartial(partial);
if (!pending) {
pending = true;
queueMicrotask(() => {
pending = false;
render();
});
}
}
Jika beberapa setState terjadi dalam task yang sama:
setState({ a: 1 });
setState({ b: 2 });
setState({ c: 3 });
Render bisa dibatch menjadi satu microtask.
22.1 Danger
Jika framework atau app mengantrikan terlalu banyak microtask, rendering tertunda.
for (let i = 0; i < 100_000; i++) {
queueMicrotask(() => doSmallWork(i));
}
"Small work" dikali 100.000 tetap besar.
23. Macro vs Micro Naming
Banyak artikel memakai istilah "macrotask". HTML standard memakai "task". Untuk komunikasi internal, boleh pakai "task vs microtask", dan hindari bergantung pada istilah "macrotask" jika sedang menjelaskan berdasarkan spec.
Ringkas:
| Informal | Lebih presisi |
|---|---|
| macrotask | task |
| microtask | microtask |
| render queue | bukan queue umum yang sama seperti task; rendering steps punya model sendiri |
| callback queue | terlalu umum |
| event queue | terlalu umum |
24. Ordering Drill
Prediksi output:
console.log("A");
setTimeout(() => console.log("B"), 0);
queueMicrotask(() => {
console.log("C");
});
Promise.resolve().then(() => {
console.log("D");
queueMicrotask(() => {
console.log("E");
});
});
console.log("F");
Output:
A
F
C
D
E
B
Reason:
- sync: A, schedule timer, schedule C, schedule D, F;
- microtask checkpoint: C, D;
- D schedules E;
- E runs before task queue lanjut;
- timer B runs in later task.
25. Rendering Drill
box.style.transform = "translateX(0px)";
requestAnimationFrame(() => {
box.style.transition = "transform 300ms ease";
box.style.transform = "translateX(100px)";
});
Apakah transisi selalu terjadi? Biasanya lebih baik daripada synchronous write, tetapi masih bisa dipengaruhi style flush dan browser optimization.
Lebih robust untuk beberapa transition setup:
box.style.transition = "none";
box.style.transform = "translateX(0px)";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
box.style.transition = "transform 300ms ease";
box.style.transform = "translateX(100px)";
});
});
Namun double rAF bukan magic universal. Gunakan ketika Anda benar-benar perlu memisahkan frame setup dan frame transition, dan sertai test visual/regression.
26. Scheduling Strategy Untuk Frontend
Pilih scheduler sesuai intent.
| Intent | Tool umum | Catatan |
|---|---|---|
| run after current sync work before paint | microtask | hati-hati starvation |
| run in later task | setTimeout, MessageChannel | memberi browser peluang lain |
| visual update before repaint | requestAnimationFrame | untuk rendering/animation |
| non-critical idle work | requestIdleCallback | butuh fallback/support check |
| cancellable network | AbortController | bukan scheduler, tapi lifecycle control |
| heavy CPU | Web Worker | off-main-thread |
| repeated visual event | rAF throttle | align ke frame |
| final user input | debounce | delay sampai user berhenti |
| periodic cap | throttle | batasi frekuensi |
27. Yielding API Utility
Utility sederhana:
export function nextTask() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
export function nextFrame() {
return new Promise((resolve) => requestAnimationFrame(resolve));
}
export async function afterPaint() {
await nextFrame();
await nextFrame();
}
Gunakan nama sesuai intent:
nextTaskuntuk memberi event loop kesempatan mengambil task lain;nextFrameuntuk align sebelum repaint;afterPaintuntuk memisahkan dua frame, misalnya setup transition.
Jangan menyebar setTimeout(..., 0) random tanpa nama. Itu membuat intent hilang.
28. Cooperative Scheduler Mini Implementation
export function createCooperativeScheduler({
budgetMs = 8,
yieldFn = () => new Promise((resolve) => setTimeout(resolve, 0)),
} = {}) {
return async function run(work) {
let shouldContinue = true;
while (shouldContinue) {
const start = performance.now();
while (performance.now() - start < budgetMs) {
shouldContinue = work();
if (!shouldContinue) {
return;
}
}
await yieldFn();
}
};
}
Usage:
const runCooperatively = createCooperativeScheduler();
await runCooperatively(() => {
const item = queue.shift();
if (!item) {
return false;
}
process(item);
return true;
});
Ini pattern penting untuk:
- process large list;
- client-side import;
- CSV parse ringan;
- offline cache migration;
- syntax highlighting;
- search indexing kecil.
Jika CPU work tetap berat, pindahkan ke Worker.
29. Web Worker Boundary
Event loop main thread tidak boleh memikul semua pekerjaan. Untuk compute besar, Worker adalah boundary.
// main.js
const worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
worker.postMessage({ type: "compute", payload: largeInput });
worker.onmessage = (event) => {
render(event.data);
};
// worker.js
self.onmessage = (event) => {
if (event.data.type !== "compute") {
return;
}
const result = expensiveCompute(event.data.payload);
self.postMessage(result);
};
Trade-off:
- serialization cost;
- transferables;
- error handling;
- worker lifecycle;
- bundler config;
- cancellation;
- memory duplication;
- API boundary design.
Part 022 akan membahas ini secara dalam.
30. Event Listener Ordering
DOM event dispatch sendiri punya model capture-target-bubble.
parent.addEventListener("click", () => console.log("parent bubble"));
parent.addEventListener("click", () => console.log("parent capture"), true);
child.addEventListener("click", () => console.log("child"));
Jika child diklik:
parent capture
child
parent bubble
Event handler berjalan dalam task event dispatch. Microtasks yang dijadwalkan di handler akan berjalan setelah stack event dispatch selesai, bukan di tengah propagation normal.
parent.addEventListener("click", () => {
queueMicrotask(() => console.log("microtask"));
console.log("parent");
});
child.addEventListener("click", () => {
console.log("child");
});
Detail event DOM lebih dalam akan dibahas di Part 008.
31. Synchronous APIs Yang Memblok Main Thread
Beberapa API terlihat ringan tetapi synchronous:
localStorage;sessionStorage;- besar
JSON.parse; - besar
JSON.stringify; - sync DOM measurement;
- alert/confirm/prompt;
- document.cookie access;
- heavy regex;
- crypto/compression JS implementation;
- large canvas readback.
Contoh buruk:
window.addEventListener("scroll", () => {
localStorage.setItem("scrollY", String(window.scrollY));
});
Lebih baik throttle dan simpan pada interval wajar, atau gunakan lifecycle event yang sesuai.
32. Event Loop dan Testing
Async UI test sering flaky karena test tidak tahu kapan sistem "settled".
Buruk:
button.click();
expect(screen.getByText("Saved")).toBeVisible();
Jika update async:
button.click();
expect(await screen.findByText("Saved")).toBeVisible();
Namun "await next tick" random juga buruk:
await new Promise((resolve) => setTimeout(resolve, 0));
Lebih baik tunggu observable user outcome:
await expect(page.getByText("Saved")).toBeVisible();
Testing strategy akan dibahas di Part 025 dan Playwright di Part 026.
33. Debugging Event Loop Bugs
Gunakan pendekatan ini.
33.1 Reconstruct Timeline
Tulis timeline:
T0: user clicks Save
T1: click handler starts
T2: set loading true
T3: fetch started
T4: handler returns
T5: render loading
T6: user clicks Cancel
T7: abort called
T8: fetch rejects AbortError
T9: catch runs
T10: final state?
33.2 Classify Each Step
Untuk setiap step:
- sync stack;
- task;
- microtask;
- rAF callback;
- network completion task;
- framework scheduler;
- worker message;
- rendering opportunity.
33.3 Identify Broken Invariant
Contoh invariant:
- only latest request may update visible data;
- cancelled request must not show error toast;
- loading indicator must be visible before heavy work;
- unmounted component must not receive state update;
- input keystroke must not run more than X ms of sync work.
34. Performance Timeline Instrumentation
Gunakan Performance API untuk memberi marker.
performance.mark("search:start");
const result = search(query);
performance.mark("search:end");
performance.measure("search", "search:start", "search:end");
console.table(performance.getEntriesByName("search"));
Untuk async:
performance.mark("save:start");
try {
await save();
performance.mark("save:success");
} catch (error) {
performance.mark("save:error");
throw error;
} finally {
performance.measure("save:total", "save:start");
}
Instrumentation membantu membedakan:
- network lambat;
- JS CPU lambat;
- rendering lambat;
- scheduling delay;
- framework delay;
- main-thread contention.
35. Timeline Example: Search Input
Failure modes:
- debounce terlalu lama;
- request lama menang race;
- parsing response besar memblok main thread;
- rendering result list terlalu mahal;
- no cancellation;
- no loading state;
- no accessibility announcement.
36. Timeline Example: Show Loading Before Heavy Work
Buruk:
button.addEventListener("click", () => {
loading.hidden = false;
heavyComputation();
loading.hidden = true;
});
Timeline:
Lebih baik:
button.addEventListener("click", async () => {
loading.hidden = false;
await nextFrame();
heavyComputation();
loading.hidden = true;
});
Namun jika heavyComputation panjang, UI tetap freeze setelah loading muncul. Lebih baik chunk atau worker.
37. Common Misconceptions
37.1 "Promise Membuat Code Parallel"
Salah. Promise merepresentasikan eventual result. Callback Promise tetap berjalan di thread JavaScript terkait.
37.2 "setTimeout(fn, 0) Berarti Segera"
Salah. Ia schedule task masa depan dengan delay minimum, bukan immediate execution.
37.3 "Microtask Selalu Lebih Baik Karena Lebih Cepat"
Salah. Microtask bisa menunda rendering dan input jika terlalu banyak.
37.4 "rAF Sama Dengan 16ms Timer"
Salah. rAF diselaraskan dengan repaint dan bisa pause/throttle.
37.5 "Jika JavaScript Single-Threaded, Tidak Ada Race Condition"
Salah. Race antar async completion sangat umum.
38. Code Review Red Flags
Cari ini:
// 1. Heavy work in input handler
input.addEventListener("input", () => {
expensiveWork();
});
// 2. Infinite microtask chain
Promise.resolve().then(function loop() {
return Promise.resolve().then(loop);
});
// 3. Loading state not painted before heavy work
setLoading(true);
expensiveWork();
setLoading(false);
// 4. No guard against stale fetch response
const data = await fetchData(id);
setData(data);
// 5. Timer as source of truth
setTimeout(expireSession, sessionExpiresInMs);
// 6. rAF for non-visual domain polling
requestAnimationFrame(pollServer);
// 7. localStorage in high-frequency event
window.addEventListener("scroll", saveToLocalStorage);
// 8. Random await timeout in test
await new Promise((resolve) => setTimeout(resolve, 100));
39. Production Heuristics
- Keep user input handlers short.
- Do not run unbounded loops on main thread.
- Treat microtasks as urgent and small.
- Use rAF for visual coordination, not business scheduling.
- Use timestamp/domain state as source of truth, not timer precision.
- Guard async requests with version or cancellation.
- Chunk CPU work if it must stay on main thread.
- Move heavy compute to Worker.
- Instrument timeline before guessing.
- Test user-visible outcomes, not internal ticks.
40. Practice Loop Kaufman
Drill 1 — Ordering
Untuk setiap snippet:
- prediksi output;
- klasifikasikan setiap callback sebagai sync/task/microtask/rAF;
- jalankan;
- perbaiki mental model.
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => {
console.log("promise 1");
queueMicrotask(() => console.log("queued"));
});
Promise.resolve().then(() => console.log("promise 2"));
console.log("end");
Drill 2 — Paint Visibility
Buat halaman kecil dengan tombol:
- klik tombol;
- tampilkan loading;
- jalankan CPU loop 500ms;
- amati apakah loading terlihat;
- ubah dengan
requestAnimationFrame; - ubah dengan chunking;
- ubah dengan worker.
Catat difference.
Drill 3 — Race Condition
Buat fake API:
function fetchUser(id, delay) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id }), delay);
});
}
Simulasikan:
selectUser("A", 500);
selectUser("B", 100);
Pastikan UI akhirnya menampilkan B, bukan A. Implementasikan dengan:
- version guard;
- AbortController-like cancellation;
- state machine event reducer.
41. Mini Lab: Build Event Loop Trace Logger
function log(label) {
console.log(`${performance.now().toFixed(2)}ms ${label}`);
}
log("sync:start");
setTimeout(() => {
log("task:timeout");
}, 0);
queueMicrotask(() => {
log("microtask:queueMicrotask");
});
Promise.resolve().then(() => {
log("microtask:promise");
});
requestAnimationFrame(() => {
log("raf");
});
log("sync:end");
Extend:
- tambahkan nested microtask;
- tambahkan nested timer;
- tambahkan CPU loop 100ms;
- tambahkan DOM mutation;
- lihat timeline di DevTools Performance panel.
Tujuannya bukan menghafal output semua browser, tetapi memahami pola scheduling.
42. Mini Lab: Cooperative List Processor
async function renderLargeList(items, container) {
container.textContent = "";
const fragment = document.createDocumentFragment();
for (let index = 0; index < items.length; index++) {
const row = document.createElement("div");
row.textContent = items[index].name;
fragment.append(row);
if (index % 250 === 0) {
container.append(fragment.cloneNode(true));
fragment.textContent = "";
await nextTask();
}
}
container.append(fragment);
}
Masalah pada contoh ini: cloneNode(true) bisa mahal dan semantic-nya mungkin salah. Tugas Anda adalah memperbaiki lab menjadi:
- append batch fragment tanpa clone;
- ukuran batch berdasarkan waktu;
- cancellation jika user berpindah route;
- progress indicator;
- performance marks.
Sketch lebih baik:
async function renderLargeList(items, container, signal) {
container.textContent = "";
let index = 0;
while (index < items.length) {
if (signal.aborted) {
return;
}
const start = performance.now();
const fragment = document.createDocumentFragment();
while (index < items.length && performance.now() - start < 8) {
const row = document.createElement("div");
row.textContent = items[index].name;
fragment.append(row);
index += 1;
}
container.append(fragment);
await nextTask();
}
}
43. Mini Case Study: Stale Request Overwrites Latest UI
Symptom
User memilih customer B, tetapi UI menampilkan customer A.
Timeline
Fix Dengan Version
let selectedCustomerVersion = 0;
async function selectCustomer(customerId) {
const version = ++selectedCustomerVersion;
setState({ status: "loading", customerId });
try {
const customer = await api.fetchCustomer(customerId);
if (version !== selectedCustomerVersion) {
return;
}
setState({
status: "success",
customerId,
customer,
});
} catch (error) {
if (version !== selectedCustomerVersion) {
return;
}
setState({
status: "error",
customerId,
error,
});
}
}
Invariant
Only the latest selected customer may update the visible customer panel.
44. Mini Case Study: Microtask Starvation Dari Library Internal
Symptom
Aplikasi freeze setelah import data kecil-menengah.
function processQueue(queue) {
if (queue.length === 0) {
return;
}
const item = queue.shift();
process(item);
queueMicrotask(() => processQueue(queue));
}
Root Cause
Microtask chain terus mengisi dirinya sampai queue habis. Browser tidak punya kesempatan memproses rendering atau input jika queue besar.
Fix
Gunakan task yielding dengan budget.
async function processQueue(queue) {
while (queue.length > 0) {
const start = performance.now();
while (queue.length > 0 && performance.now() - start < 8) {
process(queue.shift());
}
await nextTask();
}
}
Lesson
Microtask untuk "soon", bukan untuk "do all background work".
45. Self-Assessment
Anda siap lanjut ke Part 005 jika bisa menjawab:
- Apa beda task dan microtask?
- Mengapa Promise callback berjalan sebelum timer callback?
- Mengapa microtask bisa membuat UI freeze?
- Mengapa loading indicator bisa tidak terlihat sebelum CPU work?
- Kapan memakai rAF, bukan setTimeout?
- Kapan debounce tidak cukup?
- Bagaimana stale response bisa menimpa state terbaru?
- Mengapa
awaitmembuka async boundary? - Bagaimana cara chunking CPU work di main thread?
- Apa tanda bahwa pekerjaan harus dipindah ke Worker?
46. What Comes Next
Part 005 akan membahas Async Control Flow and Cancellation:
- Promise failure semantics;
- structured async flow;
- AbortController;
- timeout;
- retry/backoff;
- request deduplication;
- idempotency;
- cancellation propagation;
- async resource cleanup;
- failure-mode modeling untuk UI production.
47. References
- WHATWG HTML Living Standard, Web application APIs and event loops: https://html.spec.whatwg.org/multipage/webappapis.html
- MDN, In depth: Microtasks and the JavaScript runtime environment: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth
- MDN, Using microtasks in JavaScript with queueMicrotask(): https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
- MDN, requestAnimationFrame(): https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
- MDN, requestIdleCallback(): https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
- MDN, AbortController: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
- web.dev, Optimize long tasks: https://web.dev/articles/optimize-long-tasks
You just completed lesson 04 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.