% js-event-loop.tex — the JavaScript event loop: call stack, task vs microtask queues,
% the browser loop with rendering steps, Node's libuv phases + nextTick, setImmediate vs
% setTimeout(0), a traced output-order puzzle, long tasks and yielding, RN's JS thread.
% Source: own knowledge (no repo sources). Senior-interview level.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/runtime/js-event-loop.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=runtime kind=concept level=core platform=web new=no round=typescript-2026-09-24 topic=concurrency,performance
% @tags: event-loop, microtask, macrotask, queuemicrotask, requestanimationframe, libuv-phases, process-nexttick, setimmediate, settimeout-clamping, long-tasks, scheduler-yield, starvation
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{TSSheet}{
  morekeywords={import,from,export,default,const,let,type,interface,extends,function,
    return,async,await,if,else,switch,case,new,true,false,null,undefined,typeof,keyof,
    as,satisfies,never,string,number,boolean,void,declare,namespace,readonly,for,while},
  sensitive=true, morecomment=[l]{//}, morecomment=[s]{/*}{*/},
  morestring=[b]", morestring=[b]', morestring=[b]`}

\tikzset{
  sb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm},
  mt/.style={sb, draw=sheetOrange, fill=sheetOrange!10},
  rd/.style={sb, draw=sheetGreen!70!black, fill=sheetGreen!10},
  ph/.style={sb, minimum width=17mm},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
  pt/.style={font=\bfseries\small, anchor=west},
}

\begin{document}

\sheettitle{The JavaScript event loop — browser and Node}{runtime · memo}

\oneliner{One thread runs JS \textbf{to completion}; when the \textbf{call stack} empties the
host takes \textbf{one task} (macrotask), then \textbf{drains the whole microtask queue}, then
(browser) maybe \textbf{renders}. Node's loop is libuv's \textbf{phases}, with
\texttt{process.nextTick} and then microtasks drained \textbf{after every callback}.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  \draw[sheetGrey!40] (8.25,3.0) -- (8.25,-0.75);
  % ── browser ──
  \node[pt] at (0,2.85) {\textcolor{sheetBlue}{1} browser (HTML spec loop)};
  \node[sb, minimum width=24mm] (T) at (1.25,1.95) {task queues\\timers · I/O · input · msgs};
  \node[sb, minimum width=21mm] (R) at (4.0,1.95) {run \textbf{one} task\\to an empty stack};
  \node[mt, minimum width=24mm] (M) at (6.75,1.95) {drain \textbf{all} microtasks\\\texttt{then} · \texttt{await} · \texttt{queueMicrotask} · MO};
  \node[sb, minimum width=22mm, draw=sheetGrey, fill=white] (Q) at (6.75,0.85) {render opportunity?\\(vsync, $\approx$16.7 ms @60 Hz)};
  \node[rd] (raf) at (6.75,-0.3) {rAF cbs};
  \node[rd] (sty) at (5.2,-0.3) {style};
  \node[rd] (lay) at (4.0,-0.3) {layout};
  \node[rd] (pnt) at (2.75,-0.3) {paint};
  \draw[flow] (T) -- (R); \draw[flow] (R) -- (M); \draw[flow] (M) -- (Q);
  \draw[flow] (Q) -- node[lbl, right]{yes} (raf);
  \draw[flow] (raf) -- (sty); \draw[flow] (sty) -- (lay); \draw[flow] (lay) -- (pnt);
  \draw[flow] (pnt.west) -| (T.south);
  \draw[flow] (Q.west) -- node[lbl, above]{no} (T.south east);
  \node[lbl, text=sheetRed] at (3.9,0.35) {a microtask that keeps queueing\\microtasks never lets this run};
  \node[lbl, anchor=west] at (0,-0.65) {\texttt{requestIdleCallback} runs after the frame, if time is left};
  % ── node ──
  \node[pt] at (8.35,2.85) {\textcolor{sheetBlue}{2} Node (libuv phases)};
  \node[ph] (ti) at (12.35,2.3) {\textbf{timers}\\\texttt{setTimeout}/\texttt{Interval} due};
  \node[ph] (pe) at (14.85,1.75) {\textbf{pending}\\deferred sys errors};
  \node[ph, draw=sheetGrey, fill=white] (id) at (14.85,0.55) {idle, prepare\\(internal)};
  \node[ph, minimum width=24mm] (po) at (12.35,-0.15) {\textbf{poll}: wait on epoll/kqueue,\\run I/O callbacks};
  \node[ph] (ch) at (9.85,0.55) {\textbf{check}\\\texttt{setImmediate}};
  \node[ph] (cl) at (9.85,1.75) {\textbf{close}\\\texttt{'close'} events};
  \draw[flow] (ti) -- (pe); \draw[flow] (pe) -- (id); \draw[flow] (id) -- (po);
  \draw[flow] (po) -- (ch); \draw[flow] (ch) -- (cl); \draw[flow] (cl) -- (ti);
  \node[mt, minimum width=27mm] at (12.35,1.15) {after \textbf{every} callback:\\1 \texttt{nextTick} queue\\2 microtask queue\\repeat until both empty};
  \node[lbl, text=sheetGrey] at (12.35,-0.62) {poll blocks here when nothing is due (timeout = next timer)};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}\raggedright
  \item \textbf{Stack} of frames, objects in the GC \textbf{heap}; nothing interrupts running
        JS (run-to-completion, no data races).
  \item \textbf{Task} (macrotask) sources: timers, network/I/O, user input, \texttt{postMessage}
        /\texttt{MessageChannel}, \texttt{setImmediate} (Node). The loop picks \textbf{one}.
  \item \textbf{Microtasks}: promise reactions (\texttt{then}/\texttt{catch}/\texttt{finally},
        each \texttt{await} resumption), \texttt{queueMicrotask}, \texttt{MutationObserver}.
        Checkpoint = run until the queue is \textbf{empty}, including ones queued meanwhile
        $\Rightarrow$ \textbf{starvation}: \texttt{function f()\{Promise.resolve().then(f)\}}
        freezes the page (no task, no paint). A \texttt{setTimeout} loop does not.
  \item \textbf{Rendering} is a loop step, not a task; rAF = ``just before the next paint''
        (paused in background tabs). Reading layout after a write forces sync layout.
  \item \textbf{\texttt{setTimeout(f,0)}} = ``a task, no earlier than now''. HTML clamps to
        \textbf{$\geq$4 ms once nesting level $>$5}; background tabs throttle to $\geq$1 s.
  \item \textbf{Node}: \texttt{setTimeout(0)} is coerced to 1 ms. Since \textbf{Node 11},
        nextTicks + microtasks run between \emph{each} timer/immediate callback (as browsers
        do), not once per phase. Recursive \texttt{nextTick} starves I/O like microtasks.
  \item \textbf{\texttt{setImmediate} vs \texttt{setTimeout(0)}}: in the main module the order
        is \textbf{nondeterministic} (is 1 ms already up?). Inside an I/O callback
        \textbf{immediate always wins}: poll $\to$ check comes before timers.
\end{itemize}

\section{Puzzle — trace it}
\begin{lstlisting}[language=TSSheet]
console.log('A');
setTimeout(() => console.log('B'), 0);        // task
Promise.resolve().then(() => {                // micro [C]
  console.log('C');
  queueMicrotask(() => console.log('D'));     // micro, queued by C
});
(async () => { console.log('E');              // body runs sync
  await null; console.log('F'); })();         // micro [C,F]
process.nextTick(() => console.log('G'));     // Node only
console.log('H');
\end{lstlisting}
\begin{itemize}\raggedright
  \item Sync: \textbf{A E H}. Micro queue \texttt{[C,F]}; C runs, appends D $\to$ \texttt{[F,D]}.
  \item \textbf{Browser} (no G line): \textbf{A E H C F D B}.
  \item \textbf{Node, CommonJS}: tick queue first $\to$ \textbf{A E H G C F D B}.
  \item \textbf{Node, ES module}: the body runs inside a promise job, microtasks drain
        first $\to$ \textbf{A E H C F D G B}.
\end{itemize}

\begin{lstlisting}[language=TSSheet]
setTimeout(() => log('t'), 0); setImmediate(() => log('i'));
// main module: "t i" OR "i t" -- depends on process timing
fs.readFile(file, () => {           // we are in the poll phase
  setTimeout(() => log('t2'), 0);
  setImmediate(() => log('i2'));    // always: i2, t2
});
\end{lstlisting}

\section{Long tasks and yielding}
\begin{itemize}\raggedright
  \item \textbf{Long task} = $>$50 ms on the main thread: input, paint, rAF all wait (INP).
  \item \texttt{await} on a resolved promise does \textbf{not} yield — it is a microtask.
        Yield = end the task: \texttt{scheduler.yield()} (Chromium; keeps priority),
        \texttt{scheduler.postTask(fn,\{priority\})}, \texttt{setTimeout(r,0)} (clamped),
        \texttt{MessageChannel} (unclamped; React's scheduler, $\approx$5 ms slices).
        Chunk: every N items \texttt{await new Promise(r =\kern0pt> setTimeout(r, 0))}. Or move
        it off-thread: Web Worker / \texttt{worker\_threads}.
\end{itemize}

\section{React Native and iOS}
\begin{itemize}\raggedright
  \item RN runs one JS event loop on its \textbf{JS thread} (Hermes, microtasks real); a long
        task there drops JS frames and delays touch handling while UI-thread animations keep
        going — budgets in \emph{rn-performance}, threads and JSI in \emph{rn-architecture}.
  \item iOS: main \texttt{RunLoop} $\approx$ the loop; \texttt{DispatchQueue.main.async}
        $\approx$ a task; the Core Animation commit at the end of a turn $\approx$ the render
        step. No microtask queue: a \texttt{@MainActor} hop is enqueued work.
\end{itemize}

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{``\texttt{setTimeout(0)} runs next'' — no: every microtask (and in Node every
        nextTick) goes first, then the clamp.}
  \trap{\texttt{new Promise(executor)} runs the executor \textbf{synchronously}; only the
        reactions are async.}
  \trap{\texttt{async} does not mean ``another thread'': CPU work in an async function still
        blocks the loop.}
  \trap{\texttt{nextTick} fires \emph{before} promises in CJS but \emph{after} them in ESM
        top-level code; prefer \texttt{queueMicrotask} (portable).}
\end{itemize}

\section{Remember}
\emph{One task $\to$ all microtasks $\to$ maybe paint. Node: phases, and nextTick $\to$ micro after every callback.}

\section{Likely questions}
\begin{enumerate}\raggedright
  \item Microtask vs task? — micro drains fully after each task/callback; tasks run one per turn.
  \item Why does the UI freeze with only promises? — the checkpoint never ends, so no render step.
  \item \texttt{setImmediate} vs \texttt{setTimeout(0)}? — check vs timers phase; ordered only inside I/O callbacks.
  \item How to split a 2 s job? — chunk + yield via a task (\texttt{scheduler.yield}), or a worker.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} js-engines-performance · node-runtime ·
rn-architecture · rn-performance · Swift concurrency / \texttt{RunLoop} · GCD queues}

\end{document}
