completefrontend
Open lab My progress
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 frame can come: after a task and its microtasks, at a time the browser chooses.
  • 02
    Use rAF correctlyUse requestAnimationFrame for visual changes, and explain its timestamp and why it runs before paint.
  • 03
    Avoid extra layout workRead sizes before making changes, group visual changes in one frame, and plan for hidden tabs.

Frames between tasks

A browser does not update the screen after every line of JavaScript. It works in turns:

  1. It runs one task, such as a click handler or a timer callback, all the way to the end.
  2. It runs every microtask that the task created, such as promise callbacks.
  3. Then it may update the screen.

That chance to update the screen is called a rendering opportunity. A frame is one new picture of the page. At a rendering opportunity, the browser can get a new frame ready and draw it. Drawing it is called painting.

One turn of the loop. First a task runs to the end. Then all its microtasks run. Then, only if the browser chooses, a frame is drawn.taskmicrotasksa frameruns to the endall of them runonly if thebrowser chooses
The browser never draws in the middle of your code. A task finishes, its microtasks finish, and only then can the browser draw a new frame.
Definition

A rendering opportunity is a moment, chosen by the browser, when a page on screen can update what it shows. It does not come after every task. The screen's speed, how busy the page is, and whether the page is visible all change how often it comes.

This lesson builds on the HTML event loop. That lesson explained tasks and microtasks. This lesson adds the frame:

  • where drawing work fits between tasks,
  • why requestAnimationFrame is special,
  • why grouping page changes keeps a page smooth.

JavaScript does not draw the screen. The browser does, and it chooses when. Your code's job is to change the page at the right moment. Then the browser works out the styles, the layout, and the pixels.

Rendering opportunities and frame rate

A screen shows a new picture many times each second. This speed is the refresh rate, measured in hertz (Hz). A 60 Hz screen shows about 60 pictures each second.

One second is 1,000 milliseconds (ms). So at 60 Hz, each picture lasts about 16.7 ms. This is a helpful rough budget for your work. It is not a promise. A faster screen, a busy page, or a hidden tab all change how often the browser draws.

How long one picture lasts at 60 pictures a secondPop out in the code editor (opens in a new tab)JavaScript
const picturesPerSecond = 60;console.log((1000 / picturesPerSecond).toFixed(1));

Line 1 stores the number of pictures each second. Line 2 divides 1,000 ms by 60 and prints 16.7. Use this number to plan, not to time things. Don't write code that expects each frame to start exactly 16.7 ms after the last one.

Real-life analogyA flipbook page

A flipbook shows one picture, then the next. Before you flip to the next page, you finish its drawing. The reader only sees finished pages. They never see your pencil moving.

In real life: One flipbook page
In JavaScript: One frame: one picture of the page
In real life: The short pause before the next flip
In JavaScript: A rendering opportunity
In real life: Finishing the drawing before you flip
In JavaScript: Changing the page inside requestAnimationFrame
In real life: Flipping more slowly
In JavaScript: The browser draws fewer frames

While a task is running, the browser cannot draw. It must wait until the task ends. So a slow task makes the page miss frames. The browser does not show a half-finished change. It shows the next complete frame, later.

requestAnimationFrame timing

requestAnimationFrame asks the browser to call your function before it draws the next frame. People often shorten its name to rAF. rAF is the usual place to get a visual change ready, for example:

  • moving a card,
  • showing a new progress value,
  • taking the next step of an animation.

rAF is not a timer. It does not make a frame happen. It only waits for the next frame that the browser chooses to draw.

Ask to run code before the next framePop out in the code editor (opens in a new tab)JavaScript
requestAnimationFrame((time) => console.log("before the next paint", time > 0));

Line 1 hands a function to the browser. When the browser is about to draw a frame, it calls the function. It passes in a timestamp: the time of this frame, in milliseconds since the page started. The timestamp is above 0, so the function prints before the next paint true.

A promise, a timer, and a frame callback in one taskPop 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");

Here is what each line does:

  1. Line 1 asks for a rAF callback.
  2. Line 2 queues a timer task with a 0 ms delay.
  3. Line 3 queues a promise callback, which is a microtask.
  4. Line 4 prints task finished right away.

One order is always the same: promise microtask prints before timer task. Microtasks run as soon as the current task ends, before the browser picks another task. The rAF callback waits for the next frame and runs just before that frame is painted.

The order of the timer and the rAF callback is not fixed. It depends on when the browser draws its next frame. So rely only on these two rules:

  • A promise callback never interrupts the task that created it.
  • A rAF callback is part of drawing a frame. It is not a regular timer task.
Checked in a real browser

The lesson's test runs in Chrome when Chrome can start. It checks two facts. First, two rAF callbacks asked for in one task get the same timestamp. Second, a promise callback from that task runs before them. It does not check how long a frame lasts, because that changes.

What happens in one frame

The HTML standard describes frame work in a long list of steps called "update the rendering". In short, the browser:

  1. runs resize events, then scroll events;
  2. checks media queries (CSS rules that depend on the screen size) and moves running CSS animations forward;
  3. runs rAF callbacks;
  4. works out style and layout (the next section explains both);
  5. sends updates to observers, objects that watch elements for you (you'll meet them below);
  6. paints the new frame.

Read the table below from top to bottom. Each row is one step of a frame, in the order the browser runs it.

The steps of one frame, in order
StepWhen it runsWhy your code might care
Resize eventsFirstYour code can react when the window or an element changes size.
Scroll eventsAfter resize eventsYour code can read the scroll position or update a small part of the page.
Media queries and animationsBefore rAFThe browser updates CSS rules that depend on the screen size, and moves CSS animations forward.
FullscreenBefore rAFThe browser handles changes into and out of fullscreen before frame callbacks.
requestAnimationFrameBefore style, layout, and paintYour code gets visual changes ready for the frame that may be painted.
Style and layoutAfter rAFThe browser works out which CSS rules apply and where each box goes.
ResizeObserverDuring style and layoutYour code gets size changes. The browser may then need to do layout again.
IntersectionObserverAfter layoutYour code learns which elements are in view, before paint.
PaintLastThe browser draws the new picture of the page.

This order is why rAF is the place to get a change ready. If a rAF callback changes a class or a transform, the style and layout steps right after it include that change.

Putting code in rAF does not fix everything, though. If the callback changes the page and then asks for a size, the browser must still work out the layout early to answer. So you still need to group reads and writes. The section "Batch work into one frame" shows how.

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

Step through a small model of one task, its microtask, and one frame.

Running in
  1. script
Next: line 1
 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(" | "));
CallStoreChangeResultBlue line: runs next. 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

People often say "render" for all of the browser's drawing work. It is really three separate jobs:

  • Style: decide which CSS rules apply to each element.
  • Layout: work out each box's size and position on the page.
  • Paint: draw the pixels you see.
Change how the cart looks inside 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");});
  1. Line 1 finds the cart element on the page.
  2. Line 2 asks the browser to run a function before the next frame.
  3. Line 3 adds the class is-open to the cart. The ?. skips this if no cart was found.
  4. Line 4 prints cart state prepared when the function runs.

After the function ends, the browser decides which style rules apply. It works out the layout if needed. Then it paints the new result.

Inside one frame, in order: your rAF callback changes the page, then style decides which CSS rules apply, then layout works out sizes and positions, then paint draws the pixels.rAFstylelayoutpaintyour codechanges itwhich CSSrules applysizes andpositionsdraw thepixels
Your requestAnimationFrame callback runs first, so its change is ready in time. Then the browser does style, layout, and paint. The full list has more steps; these are the four your page changes meet.

Some CSS changes cost less than others. Changing transform (which moves or scales an element) often does not change the layout of the boxes around it. Changing width may need a new layout. This is a useful clue, not a rule. Not every transform is free, and not every width change is slow. Measure a real interaction before you rewrite it.

A tiny model of one turn and one framePop 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(" | "));

This small model writes one turn of the loop as text. It records the task first, then its microtask, then the frame steps. It leaves out most of the browser's real steps on purpose, to show one thing clearly: your task finishes before the browser can start frame work.

Resize, scroll, and observers

Resize and scroll events don't fire at random points in your code. They run at the start of frame work: resize events first, then scroll events. After that, the browser checks media queries. So a page reacts to a new window size in a steady, predictable order.

An observer is an object that watches elements and calls your function when something about them changes. You don't have to keep checking yourself.

Watch a card's size without checking again and againPop 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);
  1. Line 1 finds the card element.
  2. Line 2 makes a ResizeObserver, an observer that watches sizes. Its function gets a list of entries, one for each watched element that changed.
  3. Line 3 prints the card's width from the first entry.
  4. Line 4 closes the function.
  5. Line 5 starts watching the card, if it was found.

The browser calls the ResizeObserver function during frame work, after style and layout. That is why it can report the card's current size. You don't need a timer to check the size again and again.

IntersectionObserver runs a little later in the same frame work, after layout. It tells you when an element starts or stops overlapping the visible part of the page.

Keep the work in both observers small. If a ResizeObserver function keeps changing sizes, the browser must do layout again. It may then report an error about a "ResizeObserver loop".

Find out when a product card comes into viewPop 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);
  1. Line 1 finds the product card.
  2. Line 2 makes an IntersectionObserver. Its function also gets a list of entries. [entry] takes the first one out of the list.
  3. Line 3 prints product is visible when the card is in view.
  4. Line 4 closes the function.
  5. Line 5 starts watching the card.

This is simpler than checking on every timer tick whether the card is visible.

Batch work into one frame

The DOM is the browser's list of everything on the page. Your code does two kinds of things with it:

  • A read asks a question, such as "how wide is this element?"
  • A write changes something: a class, a style, some text, or the elements themselves.

After a write, the layout is out of date. If you read a size right after, the browser must stop and work out the layout early to answer you. Code that writes, reads, writes, and reads again makes the browser do this over and over. This is called layout thrashing.

Left: mixed order, read, write, read, write, needs 2 layouts in the lesson's model. Right: reads first, read, read, write, write, needs 1 layout.Mixed orderreadwritereadwrite2 layoutsReads firstreadreadwritewrite1 layout
In the lesson's counting model, a read right after a write makes the browser work out the layout early. Mixed order needs 2 layouts; reads first needs only 1.
Read all sizes 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");});
  • Line 1 reads the price element's width. getBoundingClientRect() gives an element's size and position.
  • Line 2 reads the cart element's width. Both reads come before any write.
  • Line 4 asks the browser to run the writes before the next frame.
  • Line 5 sets the cart's width to the two widths added together.
  • Line 6 adds the class has-total to the cart.

This code does not promise an exact number of layouts. It gives the browser one clear group of writes, instead of mixing reads and writes.

A common pattern for handling input:

  1. Each input event runs as its own task. Save the newest value.
  2. Queue one rAF callback, if none is waiting yet.
  3. In that callback, use the newest value once to change the page.

If ten pointer moves arrive before the next frame, the page changes once, not ten times.

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

Replay a small model that counts layouts. Real browsers have more detail, but the rule is real: read first, then group the writes.

Running in
  1. script
Next: line 1
 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));
CallStoreChangeResultBlue line: runs next. 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: where page changes run, and in what order
The layout counterJavaScript
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"]));
What the model countsin a task
actionsread -> read -> write -> write

A read asks for a size. A write changes a style or the page.

layouts1

Reading everything first avoids a read right after a write.

runs intask

The work runs in an ordinary task. The browser still chooses when the next frame comes.

Try it yourself

The work runs in an ordinary task. The browser still chooses when the next frame comes. This order needs 1 layout in the lesson's model. The other order needs 2.

This small model counts a layout each time a read comes right after a write. It teaches a useful habit. It is not an exact record of what your browser does.

Rendering in background tabs

A hidden page is a page in a tab you are not looking at. Browsers do less work for hidden pages, to save battery and processing power:

  • rAF callbacks usually stop until the page is visible again.
  • Timers are throttled: the browser runs them less often than you asked.

So neither rAF nor timers can tell you how much real time has passed.

Left: a visible tab. rAF runs and timers run normally. Right: a hidden tab. rAF is paused and timers are slowed down.visible tabrAFrunstimers: normalhidden tabrAFpausedtimers: slowed
When you switch to another tab, the browser saves power. It stops calling requestAnimationFrame and runs timers less often.
A small model of a hidden pagePop 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 says the page is not visible.
  • Line 2 says animation can run only when the page is visible. Here it can't.
  • Line 3 picks "throttled scheduling", because the page is hidden.
  • Line 5 prints rAF is paused.
  • Line 6 prints throttled scheduling.

Real browsers have more rules than this model. The rule for your code is still simple: when the page is visible again, carry on from the current state. Don't expect the missed animation steps to have happened.

The visibilitychange event fires when a page is hidden or shown. Use it to pause extra work, or to refresh the display when the page comes back. To find out how much time has passed, read a real clock when work starts again, such as performance.now(), which gives the time in milliseconds. Don't count rAF calls and treat them as seconds.

Practical page updates

Say a shopping cart panel follows the pointer while a reader drags it open. Here is the plan:

  1. Each pointer move runs as a task. Its handler saves the newest pointer position.
  2. If no rAF callback is waiting, the handler queues one.
  3. The rAF callback reads the saved position once and moves the cart once, for the next frame.
Move the cart at most once per 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 newest x position, starting at 0. Line 2 stores whether a rAF callback is already waiting.
  • Line 4 runs a function on every pointer move.
  • Line 5 saves the newest position. event.clientX is the pointer's distance from the left edge of the window.
  • Line 6 stops here if a callback is already waiting.
  • Lines 7 and 8 mark a callback as waiting and queue it.
  • Line 9 makes the visual change. It sets the CSS variable --cart-x to the newest position.
  • Line 10 clears the waiting flag, so a later pointer move can queue a new callback.
  • Line 11 prints the position that was used.

So the page changes at most once per frame, however many pointer moves arrive.

A few more habits help:

  • Keep the rAF callback short.
  • Get values ready in ordinary tasks when you can.
  • Avoid slow page searches, such as document.querySelector, inside an animation that runs every frame.
  • Stop the animation when its part of the page is removed.

To learn more, see rendering performance, animation, observers, and the 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 callback by when it runs: as a microtask, as a later task, or as part of drawing a frame. Read the explanation after each one.

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

Common misconceptions

  • "rAF is an exact 60 fps timer." (fps means frames per second.) rAF is a function the browser calls before it draws a frame. How often it runs, and whether it runs at all, depends on the browser and the screen.
  • "A timer with a 0 ms delay runs right away." It queues a later task. That task runs only after the current task and its microtasks finish.
  • "Every change to the page is drawn right away." The browser collects changes and decides when to draw them.
  • "rAF removes all layout cost." It puts visual work at a good time. But mixing reads and writes inside it can still cause extra layout work.
  • "Background tabs keep animations running." Hidden pages usually pause rAF and run timers less often.

Each row below is a common belief. The middle column says what is true. The last column says what the tool does not promise.

Common beliefs about rAF and timers, corrected
The beliefWhat is trueWhat it does not promise
rAF always runs every 16.7 msOn a visible page with a 60 Hz screen, frames often come about that far apart.An exact time between frames, or a callback on every screen refresh.
rAF is a timerIt is a callback tied to drawing frames.A general way to schedule background work.
One change means one paintThe browser can collect many changes and draw them together.That the pixels change the moment your code runs.
Hidden tabs animate normallyBrowsers usually pause rAF and run timers less often.A safe way to keep an animation moving.

Each row below is one way to run code later. Read across to see when it runs and what it is good for.

When each kind of callback runs
ToolWhen it runsGood for
Promise callbackAs a microtask, right after the current task.Small follow-up code that must run before any later task or frame.
setTimeoutAs a later task, once its delay has passed.Work that is not visual. It does not mean "at the next paint".
requestAnimationFrameBefore style, layout, and paint, when the browser draws a frame.Getting a visual change ready for the next frame.
ResizeObserverDuring frame work, after layout.Reacting to an element's size without checking again and again.

Practice exercises

Exercise 1 · Warm-upPredict the first output

Read the code. The last timer prints a list of words joined by commas. What is the first word?

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 0 ms 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 · PracticePick the function for visual changes

      A product card should move smoothly across the screen. Which function should get its visual change ready before the next frame is drawn?

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

        Exercise 4 · PracticeBatch the DOM work

        You need two sizes and two visual changes. Type the order that avoids extra layout work.

        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

          In the lesson's model, what happens to rAF while the page is hidden?

          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 the pointer. What should the one rAF callback group together into a single update?

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

              Check your understanding

              For each question, keep three things apart: the current task, its microtasks, and a frame that may come later.

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

                Choose an answer to see the explanation.

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

                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 asked for in the same task. What can you rely on?

                Choose an answer to see the explanation.

              5. Question 5 of 6Which order avoids an extra layout in the lesson's 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

              • The browser draws a frame only after a task and its microtasks finish, never in the middle of your code.
              • At 60 Hz, 16.7 ms is a useful rough budget. rAF callbacks are not promised to run exactly that far apart.
              • rAF runs before style, layout, and paint, so it is the place to get a visual change ready.
              • Resize, scroll, media queries, animations, observers, and paint each have a fixed place in a frame.
              • Read sizes first. Then group your page changes into one rAF callback.
              • Hidden pages can pause rAF and slow timers, so counting callbacks does not measure time.

              Remember the one-liner.
              Use requestAnimationFrame to get one visual change ready before the browser's next frame.

              Coming next: Events, microtasks & ordering.

              CompleteFrontend Clear concepts. Working examples.