Event loop best explanation ever - https://youtu.be/eiC58R16hb8?si=uv2bOdlrUJJEgP1J&t=652 JS execution flow is based on an endless event loop JS executes tasks one by one starting from the oldest When there are no tasks anymore JS waits for new ones Task may come while the engine is busy, then it's queued Queue of tasks is called macrotask queue Rendering happens only after the task is completed, before another macrotask If a task takes long, the browser is blocked & raises the "page unresponsive” alert Macrotasks Scripts we call Event handlers Scripts are added to the end of the macrotask queue by setTimeout(func) with no delay Microtasks After every macrotask, tasks from microtask queue are executed It's done before running other macrotasks or rendering or event handling It guarantees that the environment is the same between microtasks (no mouse coordinate changes, no new network data, etc) Microtask is a script called by promise handlers .then/catch/finally() or queueMicrotask(func) or observers Microtasks are used behind of await as well queueMicrotask() So if we'd like to execute a function asynchronously (after the current code), but before changes are rendered or new events handled, we can schedule it with queueMicrotask(() => { func() }) Event loop sequence M a crotask (script, event handler) M i crotask (promise handlers & queueMicrotask(func) ) Render M a crotask set by setTimeout(func) ... again & again Call stack (synchronous code): 1) code1 2) setTimeout(callback2) or setInterval(callback2) 3) Promise.resolve().then(callback3) or queueMicrotask(callback3) 4) react.setState('new value') or store.dispatch('some action') 5) requestAnimationFrame(callback4) 6) code5 Execution order Call stack (synchronous code): - code1 runs - setTimeout(callback2) schedules a macrotask (runs later) - Promise.resolve().then(callback3) schedules a microtask (runs soon) - store.dispatch('some action') runs React updates state and commits DOM changes synchronously (in this same stack) React useLayoutEffect callbacks run here synchronously after DOM mutations - requestAnimationFrame(callback4) schedules a callback for the next frame - code5 runs - call stack empties Microtasks queue: - all previously queued microtasks - callback3 (Promise/queueMicrotask) runs now - (microtasks can enqueue more microtasks; the queue is fully drained before painting) - 🕒 this happens **before the browser paints** Browser rendering cycle (current frame): - style recalculation (CSS) - layout (positions and sizes) - DOM updates from React are now visible - PAINT #1!!! current frame (if anything visually changed) - React `useEffect` callbacks run after paint (asynchronously via MessageChannel or similar) Next frame: - requestAnimationFrame(callback4) runs (just before this frame's paint) - PAINT #2!!! next frame (≈16 ms at 60 Hz): - 🕒 if nothing changed in the UI, the browser may skip actual repaint work, but requestAnimationFrame() still fires on the next refresh tick (~16 ms) as long as the page is visible Next macrotask: - setTimeout / setInterval callback (→ callback2) runs in a new event-loop tick Web workers For calculations that shouldn't block the event loop, we can use Web Workers. That's a way to run code in another parallel thread Web Workers can exchange messages with the main process They have their own variables, and their own event loop. Web Workers do not have access to DOM They are useful, mainly, for calculations They can use multiple CPU cores simultaneously in NodeJS process.nextTick(func) executes function on the current iteration of the event loop, after the current operation ends, before setTimeout() and setImmediate() setImmediate(func) is the same as setTimeout(func, 0) and executes in the next iteration of the event loop, as soon as possible Examples Sequence function Cmpt0() { function func() { alert(1) // synchronous call setTimeout(() => alert(2)) // macrotask sent to the end of the queue Promise.resolve().then(res => alert(3)) // microtask alert(4) // regular synchronous call // 1 --> 4 --> 3 --> 2 } return <button onClick={func}>Click</button> } const toRender0 = <Cmpt0 /> Count to billion without setTimeout() Run whole code at one time Changes to DOM are painted after running task is completed We'll see only the last value instead of progress Code freezes the browser function Cmpt2() { const ref = React.useRef() function countToBln() { let count = 0, start = Date.now() for (let j = 0; j < 1e9; j++) count++ ref.current.innerHTML = count alert(`Done in ${Date.now() - start} ms`) } return ( <div> <div ref={ref}>0</div> <button onClick={countToBln}>Click</button> </div> ) } const toRender2 = <Cmpt2 /> Count to billion with setTimeout() Split code into parts and queue them: 1 mln + 1 mln + ... up to 1 bln Splitting with setTimeout() we make multiple macrotasks and changes are painted in-between If an onclick event appears while the engine is busy it is queued mixed together with main counting tasks Page is responsive There's in-browser minimal delay of 4ms for many nested setTimeout calls and the earlier we schedule task via setTimeout, the faster it runs function Cmpt3() { const ref = React.useRef() function func() { let i = 0, j = 0, start = Date.now() function countToMln() { for (let k = 0; k < 1e6; k++) i++ ref.current.innerHTML = i } function countToBln() { if (i < 1e9) { setTimeout(countToBln) // schedule the new call // 1000 calls j++ } if (i === 1e9) { alert(`Done in ${Date.now() - start} ms with ${j} timeout() calls`) return } countToMln() } countToBln() } return ( <div> <div ref={ref}>0</div> <button onClick={func}>Click</button> </div> ) } const toRender3 = <Cmpt3 /> All microtasks runs before render This code acts as a synchronous, window is frozen function Cmpt1() { const ref = React.useRef() let i = 0 function count() { do { i++ ref.current.innerHTML = i } while (i % 1e3 !== 0) if (i < 1e6) queueMicrotask(count) } return ( <div> <div ref={ref}>0</div> <button onClick={count}>Click</button> </div> ) } const toRender1 = <Cmpt1 /> Let event bubble Schedule an action until the event bubbled up and was handled on all levels. menu.onclick = function() { let customEvent = new CustomEvent("menu-open", { bubbles: true }) // create a custom event with the clicked menu item data setTimeout(() => menu.dispatchEvent(customEvent)) // dispatch the custom event asynchronously }