From harness-claude
Explains browser and Node.js event loop model including task/microtask queues and rendering steps. Debugs async execution order, prevents blocking UI, selects scheduling APIs like requestAnimationFrame.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Understand the browser and Node.js event loop processing model — task queues, microtask queue, rendering steps, and task prioritization — to write code that cooperates with the rendering pipeline instead of blocking it.
Detects and eliminates long tasks (>50ms on main thread) using PerformanceObserver, scheduler.yield(), postTask(), Web Workers, and requestIdleCallback to ensure UI responsiveness.
Provides decision trees and references for JavaScript/Node.js async patterns, module systems, event loop, runtime internals, and ES2024+ features like Promise.withResolvers.
Provides expertise in modern JavaScript with ES6+ features, async patterns like promises and async/await, event loops, Node.js APIs, and browser/Node compatibility. Use for building, debugging performance, and migrating legacy JS.
Share bugs, ideas, or general feedback.
Understand the browser and Node.js event loop processing model — task queues, microtask queue, rendering steps, and task prioritization — to write code that cooperates with the rendering pipeline instead of blocking it.
setTimeout, Promise.then, queueMicrotask, and requestAnimationFramesetTimeout(fn, 0) does not fire immediately and you need to understand why (4ms clamp)setTimeout, requestAnimationFrame, requestIdleCallback, or scheduler.postTask for scheduling workrequestAnimationFrame callbacksPromise.resolve().then() callback runs before the browser paintsMutationObserver callbacks fire at unexpected times relative to renderingsetInterval drift causes visual inconsistencies in animationsUnderstand the event loop processing model. Each iteration of the event loop follows this sequence:
requestAnimationFrame callbacks
b. Recalculate styles
c. Layout
d. PaintrequestIdleCallback callbacksKnow what creates tasks vs microtasks:
Tasks (macrotasks):
setTimeout / setIntervalMessageChannel.port.postMessage()fetch completion callbacks (not the Promise, but the network callback)Microtasks:
Promise.then / catch / finallyqueueMicrotask(fn)MutationObserver callbacksasync/await continuationsUse the right scheduling primitive for each job:
// Visual update — runs before next paint
requestAnimationFrame(() => {
element.style.transform = `translateX(${x}px)`;
});
// Non-urgent work — runs when browser is idle
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 5 && tasks.length > 0) {
processTask(tasks.shift());
}
});
// Yield to browser for input processing — high-priority reschedule
await scheduler.yield();
// Background priority work — low priority
scheduler.postTask(() => analytics.flush(), { priority: 'background' });
// Immediate microtask — runs before any rendering
queueMicrotask(() => cleanupState());
Never create infinite microtask loops. Microtasks drain completely before the browser can render or process input. A recursive microtask chain blocks rendering indefinitely:
// CATASTROPHIC — freezes the browser, no rendering ever occurs
function processNextItem() {
if (items.length > 0) {
processItem(items.shift());
queueMicrotask(processNextItem); // queues another microtask before render
}
}
// FIXED — yields to the event loop between items
function processNextItem() {
if (items.length > 0) {
processItem(items.shift());
setTimeout(processNextItem, 0); // schedules a task, allowing render between items
}
}
Understand setTimeout(fn, 0) clamping. Browsers clamp setTimeout to a minimum of 4ms after 5 nested calls. This means setTimeout(fn, 0) is not truly zero-delay:
// First 4 calls: ~0ms delay
// After 5th nesting: minimum 4ms delay
// For yielding: use scheduler.yield() or MessageChannel instead
const channel = new MessageChannel();
channel.port1.onmessage = () => resumeWork();
channel.port2.postMessage(null); // fires before setTimeout, no 4ms clamp
The browser does not render after every task. It renders at the display's refresh rate (typically 60Hz = every 16.67ms). Between renders, multiple tasks and microtask drains can occur. The rendering steps are:
requestAnimationFrame callbacks (in order of registration)If all rAF callbacks and rendering complete in under 16.67ms, the frame is on time. If they exceed 16.67ms, the frame is late and the user sees jank.
A data processing module used queueMicrotask to process items "asynchronously" without blocking the current task. With 10,000 items, each microtask processed one item and queued the next:
// BROKEN — 10,000 microtasks drain without rendering
function processChunk() {
if (queue.length > 0) {
process(queue.shift());
queueMicrotask(processChunk); // never yields to render
}
}
The browser froze for 2 seconds (10,000 items * 0.2ms each). No frames were painted because microtasks drain completely before rendering. Fix: replace queueMicrotask with setTimeout(fn, 0) or scheduler.yield() to yield to the event loop between chunks.
A React component's useEffect cleanup ran as a microtask (in React 18's concurrent mode). The effect modified the DOM, and the cleanup restored it. Because the cleanup ran as a microtask before paint, this sequence occurred:
element.textContent = 'Loading...'element.textContent = 'Done'The developer expected the user to see the loading state. The fix: use setTimeout in the effect to ensure the DOM update renders before the next operation.
The browser event loop has rendering steps integrated. The Node.js event loop has phases:
setTimeout, setInterval callbackssetImmediate callbackssocket.on('close')Key difference: Node.js has process.nextTick() which runs before any other microtask in the microtask queue. In the browser, queueMicrotask and Promise.then are equivalent in priority.
The Scheduler API provides three priority levels:
user-blocking — interaction responses, should run within the current frameuser-visible — updates the user will notice (default)background — analytics, prefetch, non-urgent workawait scheduler.postTask(() => updateUI(), { priority: 'user-blocking' });
await scheduler.postTask(() => prefetchData(), { priority: 'background' });
Using Promise.resolve().then() for deferral when you mean setTimeout(fn, 0). Microtasks run before rendering. If you want to defer work until after the browser paints, use setTimeout or requestAnimationFrame + setTimeout (double-rAF pattern). A Promise-based deferral runs immediately in the microtask checkpoint, before any rendering.
Infinite microtask loops. Any recursive pattern using Promise.then or queueMicrotask that does not eventually yield creates an infinite microtask loop. The browser cannot render, process input, or run timers until the microtask queue is empty.
Assuming setTimeout(fn, 0) fires immediately. After 5 nested setTimeout calls, the minimum delay is clamped to 4ms in browsers. For time-sensitive yielding, use MessageChannel or scheduler.yield() which do not have this clamp.
Using setInterval for animations instead of requestAnimationFrame. setInterval fires at fixed wall-clock intervals regardless of the display refresh rate. It can fire between frames (wasted work) or multiple times in one frame (duplicate work). requestAnimationFrame fires exactly once per frame, synchronized with the display.
Blocking the event loop with synchronous I/O in Node.js. fs.readFileSync, crypto.pbkdf2Sync, and other sync APIs block the entire event loop. All pending I/O, timers, and HTTP requests are stalled. Use async equivalents.