cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Rendering & the event loop

Learn where browser frames fit between tasks, why requestAnimationFrame runs before paint, and how to batch page updates smoothly.

By the end, you can
  • 01
    Place a frameExplain where a rendering opportunity sits after a task and its microtasks without promising a fixed schedule.
  • 02
    Use rAF correctlyChoose requestAnimationFrame for visual updates and explain its timestamp and before-paint timing.
  • 03
    Avoid extra layout workBatch DOM reads before writes, group visual changes in one frame, and plan for hidden tabs.

Frames between tasks

A browser does not paint after every JavaScript statement. It runs a task, drains the microtasks created by that task, and then may get a chance to update the screen. That chance is called a rendering opportunity. It is where a browser can prepare and paint a new frame.

Definition

A rendering opportunity is a browser-selected moment when a visible document can update its rendering. It is not a promise after every task, and it is affected by the display, page load, and whether the document is visible.

This lesson continues the HTML event loop processing model. That lesson established tasks and microtasks. Here the missing piece is the frame: where visual work fits, why requestAnimationFrame is special, and why grouping DOM work helps a page feel responsive.

The useful question is not “when does JavaScript paint?” JavaScript does not paint. The browser owns rendering and chooses its opportunities. Your code can prepare state at the right time, then let the browser calculate style, layout, and paint.

Rendering opportunities and frame rate

A display that refreshes at 60 hertz can show about 60 pictures each second. Dividing one second by 60 gives roughly 16.7 milliseconds per picture. That is a helpful mental budget, not a deadline that the web platform guarantees. A faster display, a busy page, or a hidden tab can change what opportunities the browser provides.

A 60 fps budget calculationPop out in the code editor (opens in a new tab)JavaScript
const picturesPerSecond = 60;console.log((1000 / picturesPerSecond).toFixed(1));

Line 1 names a common display rate. Line 2 divides 1,000 milliseconds by 60 and prints 16.7. This only describes a useful example. Do not write code that assumes each rAF timestamp advances by exactly 16.7.

Real-life analogyA flipbook page

A flipbook shows one picture, then another. Before the next page flips, you can fix the drawing on the next page. The important part is that you prepare the picture before it is shown, rather than expecting every pencil movement to be visible at once.

In real life: One flipbook page
In JavaScript: One possible rendered frame
In real life: A pause before the next flip
In JavaScript: A rendering opportunity
In real life: Fix the drawing before flipping
In JavaScript: Update state in requestAnimationFrame
In real life: Pages can flip less often
In JavaScript: The browser can skip or throttle opportunities

Where the analogy stops: A browser has many rendering steps and may choose when to run them; a flipbook does not model all of that scheduling.

When a task runs too long, the browser cannot run its rendering steps until that task returns. This is why expensive JavaScript makes a page miss visual updates. The browser may then show the next complete frame later instead of showing a half-finished one.

requestAnimationFrame timing

requestAnimationFrame, often shortened to rAF, asks the browser to call a function before a future paint. It is the normal place to prepare a visual change such as moving a card, applying a progress value, or updating an animation state. It is not a timer and it does not make a frame happen by itself.

Queue work before the next paintPop out in the code editor (opens in a new tab)JavaScript
requestAnimationFrame((time) => console.log("before the next paint", time > 0));

Line 1 queues a callback. When the browser reaches an appropriate rendering opportunity, it calls the function with a high-resolution timestamp. The callback prints before the next paint true in a browser because the timestamp is a positive time value.

Promises, timers, and an upcoming framePop out in the code editor (opens in a new tab)JavaScript
requestAnimationFrame((time) => console.log("before the next paint", time > 0));setTimeout(() => console.log("timer task"), 0);Promise.resolve().then(() => console.log("promise microtask"));console.log("task finished");

Line 1 asks for a frame callback. Line 2 queues a timer task. Line 3 queues a promise microtask. Line 4 prints task finished immediately. The stable ordering fact is that promise microtask runs before timer task, because microtasks drain after the current task before another task is selected. The rAF callback waits for a rendering opportunity and runs before that opportunity's paint.

Do not claim a fixed order between an arbitrary timer and a particular future rAF callback. Browser scheduling can vary. Instead, use the guarantees that matter: a promise reaction does not interrupt the task that scheduled it, and rAF is part of the rendering update rather than a regular timer task.

Verified browser facts

The lesson test uses Chrome when it can start. It checks two stable facts: two rAF callbacks queued in one task receive the same timestamp, and a promise microtask queued by that task runs before its rAF callback. It intentionally does not assert frame duration.

What happens in one frame

The HTML standard's “update the rendering” algorithm has many details. The plain version is still useful: the browser handles resize and scroll-related work, updates media queries and animations, runs rAF callbacks, then calculates style and layout before it paints. Observers fit into that rendering update too.

Confirmed order inside the rendering update
StepPlain-language orderWhy code might notice
Resize eventsFirst listed author-visible workRespond to a viewport or element resize event.
Scroll eventsAfter resizeRead a current scroll position or update a small UI state.
Media queries and animationsBefore rAFThe browser updates query and animation state for this opportunity.
FullscreenBefore rAFFullscreen changes are processed before frame callbacks.
requestAnimationFrameBefore style/layout/paintPrepare visual changes for the frame that may be painted.
Style and layoutAfter rAFCompute which rules apply and where boxes belong.
ResizeObserverDuring style/layout loopReceive size changes; another layout pass may be needed.
IntersectionObserverAfter layout workReceive visibility intersection updates before paint.
PaintLastDraw the current visual result when the browser updates the display.

This order explains why rAF is a preparation point. If an rAF callback changes a class or a transform, the style and layout phase that follows can include that change. A callback that reads layout after changing it can still ask the browser for a current measurement, so placement helps but does not remove the need to batch reads and writes.

Step through a task, microtasks, and a frame
Step 0 of 7Ready
Your turn: follow the blue line

Step through an instrumented teaching model of one task, its microtasks, and one rendering opportunity.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 function runTurn(state) {  const events = [`task: ${state.task}`];  events.push(...state.microtasks.map((name) => `microtask: ${name}`));  state.clock += 16.7;  events.push("frame: requestAnimationFrame");  events.push("frame: style, layout, paint");  state.frames.push(state.clock);  return events;} console.log(runTurn(model).join(" | "));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Style, layout, and paint

Style means deciding which CSS rules apply. Layout means calculating box sizes and positions. Paint means drawing the visible pixels for the current result. They are different jobs, even though developers often say “render” for all of them.

Change a visual state in rAFPop out in the code editor (opens in a new tab)JavaScript
const cart = document.querySelector("[data-cart]");requestAnimationFrame(() => {  cart?.classList.add("is-open");  console.log("cart state prepared");});

Line 1 finds a cart element. Line 2 queues the before-paint callback. Line 3 changes one class, and line 4 prints cart state prepared when the callback runs. The browser then decides which style rules apply, computes layout if needed, and eventually paints the new result.

A property such as transform can often be updated without changing the layout of surrounding boxes, while a property such as width may require layout. That is a useful performance clue, but it is not a promise that every transform is free or every width change is bad. Measure a real interaction before rewriting it.

A tiny event-loop and frame modelPop out in the code editor (opens in a new tab)JavaScript
const state = { clock: 0, task: "change cart", microtasks: ["save cart"], frames: [] }; function runTurn(model) {  const events = [`task: ${model.task}`];  events.push(...model.microtasks.map((name) => `microtask: ${name}`));  model.clock += 16.7;  if (model.clock >= 16.7) {    events.push("frame: requestAnimationFrame");    events.push("frame: style, layout, paint");    model.frames.push(model.clock);  }  return events;} console.log(runTurn(state).join(" | "));

The model first records a task, then its microtask, then a frame phase. It deliberately compresses the browser's algorithm. Its job is to make the boundary visible: JavaScript code finishes a task before the browser can reach the next frame work.

Resize, scroll, and observers

A resize event and a scroll event are not random callbacks sprinkled between your JavaScript lines. In the rendering update, the browser runs resize steps first and scroll steps next. Media queries are evaluated after that, which lets a page react to a changed viewport consistently.

Observe a size without pollingPop out in the code editor (opens in a new tab)JavaScript
const card = document.querySelector("[data-card]");const observer = new ResizeObserver((entries) => {  console.log("card width", entries[0].contentRect.width);});if (card) observer.observe(card);

Line 1 gets the element. Line 2 creates a ResizeObserver. Line 3 logs the measured width when the browser delivers an entry. Line 4 starts observing. ResizeObserver belongs in the rendering update after style and layout work, which is why it can report an element's current size without you using a timer to check repeatedly.

IntersectionObserver comes later in the same broad update, after layout work. It tells code that an element intersects a root or viewport. Both observers should do small work. If an observer changes sizes again and again, the browser may need more layout work and can report a resize-observer loop problem.

React to a visible product cardPop out in the code editor (opens in a new tab)JavaScript
const card = document.querySelector("[data-product]");const observer = new IntersectionObserver(([entry]) => {  if (entry.isIntersecting) console.log("product is visible");});if (card) observer.observe(card);

Line 1 selects a product card. Line 2 creates an observer. Line 3 prints product is visible when the entry intersects. Line 4 closes the callback, and line 5 starts observation. This is a cleaner signal than checking visibility on every timer tick.

Batch work into one frame

DOM reads ask questions such as “what is this element's width?” DOM writes change classes, styles, text, or nodes. When code repeatedly writes and then reads, each read may need the browser to make layout current early. This pattern is often called layout thrashing.

Read first, then writePop out in the code editor (opens in a new tab)JavaScript
const priceWidth = price.getBoundingClientRect().width;const cartWidth = cart.getBoundingClientRect().width; requestAnimationFrame(() => {  cart.style.width = `${cartWidth + priceWidth}px`;  cart.classList.add("has-total");});

Lines 1 and 2 read both measurements before a write. Line 4 schedules the visual change. Lines 5 and 6 group the writes in one callback. This code does not guarantee a number of layouts, but it gives the browser one clear batch instead of interleaving read and write demands.

A common practical pattern is: handle input in its task, store the latest value, and use one pending rAF callback to apply visual changes. If ten pointer events arrive before the next frame, the callback can use the newest value once instead of doing ten separate visual updates.

Step through interleaved reads and writes
Step 0 of 5Ready
Your turn: follow the blue line

Replay an instrumented layout-cost model. Browsers have more detail, but the useful rule is real: read first, then group writes.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 function countLayouts(plan) {  let dirty = false;  let layouts = 0;  for (const action of plan) {    if (action === "write") dirty = true;    if (action === "read" && dirty) {      layouts += 1;      dirty = false;    }  }  return layouts + Number(dirty);} console.log(countLayouts(actions));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Playground: place and batch a DOM update
Read and write planJavaScript
const reads = ["measure cart", "measure price"];const writes = ["move cart", "show total"]; function countLayouts(plan) {  let dirty = false;  let layouts = 0;  for (const action of plan) {    if (action === "write") dirty = true;    if (action === "read" && dirty) {      layouts += 1;      dirty = false;    }  }  return layouts + Number(dirty);} console.log(countLayouts(["read", "read", "write", "write"]));
Model resulttask
actionsread -> read -> write -> write

Reads ask for current geometry. Writes change styles or DOM state.

layout passes1

Batching avoids the middle read after a write.

placetask

The work happens in an ordinary task; the browser still chooses when its next rendering opportunity arrives.

Try it yourself

The work happens in an ordinary task; the browser still chooses when its next rendering opportunity arrives. This batched plan needs 1 layout pass in the teaching model; the other order needs 2.

This model counts when a read follows a write. It teaches a practical habit, not an exact trace of your browser's rendering engine.

Rendering in background tabs

A hidden page is not a small visible page. Browsers save battery and CPU by reducing work for background documents. In normal browser use, rAF callbacks stop or pause when a document is hidden, and timer callbacks are throttled. Do not use either one as a wall-clock guarantee.

A simple visibility modelPop out in the code editor (opens in a new tab)JavaScript
const pageVisible = false;const animationCanRun = pageVisible;const timerPolicy = pageVisible ? "normal scheduling" : "throttled scheduling"; console.log(animationCanRun ? "rAF can run" : "rAF is paused");console.log(timerPolicy);

Line 1 models a hidden page. Line 2 says animation cannot run in that model. Line 3 chooses a throttled timer policy. Lines 5 and 6 print rAF is paused and throttled scheduling. The real browser has more policies, but the application rule is simple: resume from current state when the page becomes visible instead of expecting background animation steps to have happened.

Use visibilitychange to pause optional work or refresh a display when it becomes visible again. For elapsed time, compare a real clock value when work resumes. Do not count rAF calls and assume they represent every missed second.

Practical page updates

Suppose a shopping cart follows a pointer while a reader drags it open. The pointer event is a task. Store the latest pointer position in that handler. If no rAF callback is pending, queue one. The rAF callback reads the stored position once and writes one transform for the next frame.

One visual update per available framePop out in the code editor (opens in a new tab)JavaScript
let latestX = 0;let pending = false; window.addEventListener("pointermove", (event) => {  latestX = event.clientX;  if (pending) return;  pending = true;  requestAnimationFrame(() => {    document.documentElement.style.setProperty("--cart-x", `${latestX}px`);    pending = false;    console.log("updated cart", latestX);  });});

Line 1 stores the latest input. Line 4 updates it for every pointer task. Lines 5 through 7 ensure only one rAF callback is waiting. Line 9 makes the visual write, line 10 unlocks a future callback, and line 11 logs the value that was used. This avoids treating every input event as a separate paint request.

Keep the rAF callback short. Prepare values in ordinary tasks when possible, avoid expensive DOM queries inside a tight animation loop, and stop visual work when a component unmounts. For more page-level tools, continue with rendering performance, animation, observers, and main thread.

Put each callback in its place
  • A Promise.then callback
  • A queueMicrotask callback
  • A setTimeout callback
  • A user click handler
  • A requestAnimationFrame callback
  • A ResizeObserver notification
Try it yourself
0 of 6 correct

Sort each item by the event-loop phase it belongs to. Read the explanation after each placement.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “rAF is a precise 60 fps timer.” It is a before-paint callback. The rate and even whether it runs depend on the browser.
  • “A timer with zero delay runs immediately.” It creates a later eligible task after the current task and microtasks finish.
  • “Every DOM write paints now.” Browsers batch work and decide when to update rendering.
  • “rAF removes all layout cost.” It coordinates visual work; interleaved reads and writes can still cause extra layout work.
  • “Background tabs keep animations alive.” Hidden documents commonly pause rAF and throttle timers.
Similar scheduling tools, different jobs
IdeaWhat it really saysWhat it does not promise
rAF always runs every 16.7 msA visible 60 Hz page often has opportunities around that interval.A guarantee of a fixed duration or a callback every display refresh.
rAF is a timerIt is a visual callback tied to rendering opportunities.A general background scheduling API.
One write equals one paintWrites can be batched until the browser renders.A command that instantly updates pixels.
Hidden tabs animate normallyBrowsers commonly pause rAF and throttle timers.A safe place to depend on animation progress.
Which callback belongs where?
ToolWhere it runsGood use
Promise callbackMicrotask after current taskFinish small follow-up JavaScript before a later task or frame.
setTimeoutA later task when eligibleDo non-visual deferred work; it does not mean next paint.
requestAnimationFrameBefore style, layout, and paint at a rendering opportunityPrepare a visual update for an upcoming frame.
ResizeObserverDuring the rendering update after layoutReact to an element's measured size without polling.

Practice exercises

Exercise 1 · Warm-upPredict the first output

Read the code. What word starts the comma-separated output?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const order = [];
Promise.resolve().then(() => order.push("promise"));
setTimeout(() => order.push("timer"), 0);
order.push("task");
setTimeout(() => console.log(order.join(",")), 10);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the microtask

    Which queued callback runs before the zero-delay timer?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const order = [];
    Promise.resolve().then(() => order.push("promise"));
    setTimeout(() => order.push("timer"), 0);
    order.push("task");
    setTimeout(() => console.log(order.join(",")), 10);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeChoose the visual API

      A product card should move smoothly on screen. Which API should prepare its visual update before paint?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeBatch the DOM work

        Type the safer order for two measurements and two visual changes.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const plan = ["read", "read", "write", "write"];
        console.log(plan.join(" then "));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePlan for a hidden page

          What happens to rAF while the page is hidden in the lesson's model?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const visible = false;
          console.log(visible ? "animate" : "pause animation");

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a shopping cart

            Your cart follows a pointer. What should the rAF callback group into one update?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Answer by keeping three layers separate: the current task, its microtasks, and a possible future rendering opportunity.

              Rendering and the event loop quiz · 6 questionsScore: first tries count
              1. Question 1 of 6After a normal task finishes, what runs before a later timer task?

                Choose an answer to see the explanation.

              2. Question 2 of 6What does this print first?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const order = [];
                Promise.resolve().then(() => order.push("promise"));
                setTimeout(() => order.push("timer"), 0);
                order.push("task");
                setTimeout(() => console.log(order.join(",")), 10);

                Choose an answer to see the explanation.

              3. Question 3 of 6Why is requestAnimationFrame special?

                Choose an answer to see the explanation.

              4. Question 4 of 6Two rAF callbacks are queued in the same task. What stable fact can you rely on?

                Choose an answer to see the explanation.

              5. Question 5 of 6Which order avoids an extra forced layout in the teaching model?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const plan = ["read", "read", "write", "write"];
                console.log(plan.join(" then "));

                Choose an answer to see the explanation.

              6. Question 6 of 6What should animation code expect in a background tab?

                Choose an answer to see the explanation.

              Key takeaways

              • A frame is browser work that can happen after a task and its microtasks, not after every JavaScript line.
              • At 60 Hz, 16.7 ms is a useful rough budget, not a guaranteed rAF interval.
              • rAF runs before style, layout, and paint at a rendering opportunity, so it is for visual preparation.
              • Resize, scroll, media queries, animations, observers, and paint have defined places in the rendering update.
              • Read layout values first, then group DOM writes into one rAF callback when visual work is involved.
              • Background documents can pause rAF and throttle timers, so callback counts are not a clock.

              Remember the one-liner.
              Use requestAnimationFrame to prepare one visual update before the browser's next possible paint.

              Coming next: Events, microtasks & ordering.

              CompleteFrontend Clear concepts. Working examples.