AdvancedJavaScript · Lesson 1 of 9

The Event Loop, Tasks & Rendering

Why the page freezes, how tasks and microtasks are ordered, and where rendering fits in.

The browser runs your JavaScript, handles user input and paints the screen on one main thread. While your code runs, nothing else happens — a 2-second loop freezes clicks, typing and animations for 2 seconds.

After each task (a script, a click handler, a timer callback) the browser empties the microtask queue (Promise callbacks, queueMicrotask) completely, then may render a frame, then picks the next task. So Promise callbacks always run before setTimeout callbacks queued at the same time.

requestAnimationFrame runs just before the next paint — the right place for visual updates. Break long work into chunks and yield between them (with setTimeout, or scheduler.yield() where supported) so the page stays responsive.

order.jsJavaScript
console.log("1. script start");

setTimeout(() => console.log("5. timeout (next task)"), 0);

Promise.resolve()
  .then(() => console.log("3. microtask 1"))
  .then(() => console.log("4. microtask 2 (still before the timeout)"));

queueMicrotask(() => console.log("3b. queueMicrotask"));

console.log("2. script end");
// Order: 1, 2, 3, 3b, 4, 5
Runs in your browser · JavaScript
chunks.jsJavaScript
const yieldToBrowser = () => new Promise((resolve) => setTimeout(resolve, 0));

// Processes a big list without freezing the page for more than ~50ms at a time
async function processInChunks(items, handleItem, budgetMs = 50) {
  let deadline = performance.now() + budgetMs;
  for (const item of items) {
    handleItem(item);
    if (performance.now() > deadline) {
      await yieldToBrowser();                 // let clicks and painting happen
      deadline = performance.now() + budgetMs;
    }
  }
}

const items = Array.from({ length: 200_000 }, (_, i) => i);
let total = 0;
const started = performance.now();
const timer = setInterval(() => console.log("UI still responsive"), 100);

processInChunks(items, (n) => {
  for (let k = 0; k < 200; k++) total += Math.sqrt(n + k);
}).then(() => {
  clearInterval(timer);
  console.log(`Done in ${Math.round(performance.now() - started)}ms, total ${Math.round(total)}`);
});
Runs in your browser · JavaScript

Key points

  • One main thread: long-running JavaScript blocks input and painting.
  • Microtasks (Promises) run before the next task (timers, events).
  • Chunk long work and yield; do visual updates in requestAnimationFrame.

Exercise

Create a page with a button that sorts 2 million random numbers. Notice the freeze, then rewrite it to show a progress bar updated in requestAnimationFrame while processing in chunks.

Show solution

Try the exercise yourself first — then compare your approach with this one.

A single sort() call on 2 million numbers can't be interrupted. Scores are whole numbers from 0 to 100, so a counting sort works and splits naturally into chunks: count in slices of 100,000 values, then rebuild the sorted array, yielding to the browser between slices. The progress bar is updated in requestAnimationFrame, and the counter keeps ticking because the page never freezes.

index.htmlHTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Chunked sort</title>
    <script src="app.js" defer></script>
  </head>
  <body>
    <button id="sort">Sort 2 million scores</button>
    <progress id="progress" max="1" value="0"></progress>
    <p>Ticks while sorting: <span id="ticks">0</span></p>
    <p id="result"></p>
  </body>
</html>
Live preview in a sandboxed frame
app.jsJavaScript
const CHUNK = 100_000;
const progressEl = document.querySelector("#progress");
const yieldToBrowser = () => new Promise((resolve) => setTimeout(resolve, 0));

let ticks = 0;
setInterval(() => { document.querySelector("#ticks").textContent = ++ticks; }, 50);

function showProgress(fraction) {
  requestAnimationFrame(() => { progressEl.value = fraction; });
}

async function countingSort(values) {
  const counts = new Uint32Array(101);
  for (let start = 0; start < values.length; start += CHUNK) {
    const end = Math.min(start + CHUNK, values.length);
    for (let i = start; i < end; i++) counts[values[i]]++;
    showProgress((end / values.length) * 0.5);
    await yieldToBrowser();
  }
  const sorted = new Uint8Array(values.length);
  let pos = 0;
  for (let score = 0; score <= 100; score++) {
    sorted.fill(score, pos, pos + counts[score]);
    pos += counts[score];
    if (score % 10 === 0) {
      showProgress(0.5 + (pos / values.length) * 0.5);
      await yieldToBrowser();
    }
  }
  showProgress(1);
  return sorted;
}

document.querySelector("#sort").addEventListener("click", async () => {
  const scores = Uint8Array.from({ length: 2_000_000 }, () => Math.floor(Math.random() * 101));
  const started = performance.now();
  const sorted = await countingSort(scores);
  document.querySelector("#result").textContent =
    `Sorted ${sorted.length.toLocaleString("en-US")} scores in ${Math.round(performance.now() - started)} ms: ` +
    `lowest ${sorted[0]}, median ${sorted[sorted.length / 2]}, highest ${sorted[sorted.length - 1]}`;
});

Check your understanding

  1. In what order do these run? A: setTimeout(..., 0), B: Promise.resolve().then(...), C: plain console.log

  2. Why does a long loop freeze the page?

  3. When does a requestAnimationFrame callback run?

  4. How can you keep the page responsive during long work without a Worker?

Ask AI