PaintGENERALDEVICE-SPECIFICBROWSER-SPECIFIC

The Frame Budget

A display refreshing 60 times a second gives roughly 16.7ms per frame — a useful baseline that shrinks on faster displays and is shared with the browser's own work.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

How much time does a frame actually give me, and how much of it is genuinely mine?

The user intent

Someone drags a slider, scrolls a list, or watches a menu open. They expect the pixels to keep up with their finger; anything else registers as the software being broken rather than busy.

The obvious build

I have 16ms per frame. As long as my handler finishes in under 16ms, the animation will be smooth.

Why it breaks

The number is derived, not given: it is one divided by the refresh rate. At 60Hz that is about 16.7ms, at 90Hz about 11.1ms, at 120Hz about 8.3ms. Modern phones, tablets and laptops routinely ship high-refresh displays, and some vary the rate dynamically — so a budget memorised as "16ms" is up to twice the real one.

How it breaks in a real browser
  • The number is derived, not given: it is one divided by the refresh rate. At 60Hz that is about 16.7ms, at 90Hz about 11.1ms, at 120Hz about 8.3ms. Modern phones, tablets and laptops routinely ship high-refresh displays, and some vary the rate dynamically — so a budget memorised as "16ms" is up to twice the real one.
  • The budget is not yours. Within a frame the browser must also run style recalculation, layout, paint recording, raster scheduling and compositing, plus input handling, timers, and its own housekeeping. Your JavaScript gets what is left, and what is left is well under half in a typical frame.
  • Frames are not independent. Overrunning one does not simply drop one frame — the next frame starts late, and on a display with a fixed refresh you miss a whole refresh interval, so a small overrun produces a large stutter.
  • A budget met on your machine is not a budget met on the user's. The same handler that takes a fraction of the frame on a development laptop can exceed several frames on a mid-range phone (The Clock Is a Variable).
  • Not all frame work is on the main thread. A frame can be late because raster did not finish or because the compositor had too many layers to draw, neither of which appears in a main-thread measurement (Layer Explosion).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • A display refreshes at a fixed or variable rate. The browser aims to produce one frame per refresh, and a frame that is not ready in time is simply not shown — the previous one is shown again.
  • The budget is arithmetic: one second divided by the refresh rate. It is not a web-vital threshold, not a guideline someone chose, and not subject to revision by a working group. 60Hz gives about 16.7ms; that is the *baseline* most people learn, and it is the most generous common case rather than the typical one.
  • Inside a frame the browser runs, in order: pending input handling, then queued tasks and their microtask checkpoints, then requestAnimationFrame callbacks, then style, layout, paint recording, and finally the commit to the compositor (The Rendering Opportunity).
  • Your code occupies the task and callback portion. Style, layout and paint are proportional to what you invalidated; compositing is proportional to layer count and drawn area. So "how long may my handler run" is really "how much did my handler leave for the browser to finish the frame".
  • Because tasks run to completion, a single overrunning task cannot be preempted. The browser is not choosing to skip the frame — it is unable to produce one (Long Tasks).
  • On variable-refresh displays the rate itself moves, so the honest model is "produce frames as fast as the display asks, and know that the ask can double".

What this makes the browser do

And which of it is avoidable.

  • Per frame, unavoidably: dispatching input, running due timers and callbacks, draining microtasks, recalculating style for invalidated elements, running layout if geometry changed, recording display lists for damaged regions, and compositing the layer tree.
  • Scheduling raster for damaged tiles, which happens off the main thread but must complete before the frame that needs it.
  • Garbage collection, which the engine tries to schedule into idle time but will run when it must — and which is why a frame occasionally costs far more than its inputs suggest.
  • Avoidable: style and layout for elements nothing changed in, repeated re-renders of components whose inputs are identical, and per-frame work that could have been delegated to the compositor entirely (Cheap and Expensive Animation).

Where the number comes from, and who else is spending it

SIMULATEDThe proportions in this timeline are an Engineer Atlas teaching model, not a measurement of any page: real splits vary enormously by page and device, and a page with a heavy layer tree can spend far more in composite while a page with a static DOM can spend nothing in layout. Use the shape as a mental model and your own frames track for numbers.

The frame budget is division. One second divided by the display's refresh rate is how long the browser has to produce each frame. At 60Hz that is approximately 16.7ms — the figure almost everyone learns, and a genuinely useful baseline because it is the most common and the most generous of the rates you will meet. At 90Hz it falls to about 11.1ms, and at 120Hz to about 8.3ms. High-refresh displays are now ordinary on phones, tablets and laptops, and some of them vary the rate while the page is running.

The second correction is bigger than the first. That budget is the *browser's*, not yours. Inside it the browser must dispatch input, run due timers and callbacks, drain microtasks, recalculate style for everything you invalidated, run layout if geometry moved, record paint commands for damaged regions, and commit to the compositor. Your JavaScript is one item in that list, and if it uses most of the frame there is nothing left for the rest.

So the practical statement is: the frame budget is one over the refresh rate, and your share of it is a minority. The timeline below shows that shape in relative units — the proportions are the teaching, not any specific measurement.

One frame, and who spends itrelative units — proportions within one frame, not milliseconds
JS
Style
Layout
Paint
Composite
  • JSInput handlers, timers, promise continuations, framework render, requestAnimationFrame callbacks. The only part you write — and the part that must leave room for everything after it.
  • StyleRecomputing values for every element your changes invalidated. Cost tracks the invalidated set, not the DOM size (Style Invalidation).
  • LayoutOnly if geometry changed — but when it does, it can cascade across siblings and ancestors (Layout Thrashing).
  • PaintRecording display lists for the damaged regions. Raster of those lists happens off this thread but must finish before the frame ships (Paint Commands).
  • CompositeAssembling the layer tree into the frame. Scales with layer count and drawn area, and happens whether or not you changed anything (Compositing Layers).

The whole bar is one frame at the display's refresh rate. Overrun anywhere and the frame is not shown — the previous one is repeated, and the next frame starts late.

What has to happen inside one frame

The order matters as much as the total. Style cannot start until your callbacks have finished, because they may change more styles. Layout cannot start until style is resolved. Paint cannot record until geometry is known. Anything you do that reaches backwards — reading a computed layout value after writing a style — forces the browser to run an earlier stage again, inside the same frame.

The frame, in order
  1. 1
    Input dispatch

    Deliver pending pointer, keyboard and scroll events to listeners.

    fails by A non-passive listener on a scroll or touch event delays the scroll until the listener returns (Passive Listeners).

  2. 2
    Tasks and microtasks

    Run one task to completion, then drain the microtask queue fully.

    fails by A task that runs long cannot be preempted; a microtask chain that keeps queueing blocks the frame indefinitely (The Microtask Checkpoint).

  3. 3
    Animation callbacks

    Run requestAnimationFrame callbacks, which are meant for per-frame visual updates.

    fails by Doing non-visual work here, or reading layout mid-callback and forcing a synchronous recalculation.

  4. 4
    Style

    Compute final values for every invalidated element.

    fails by An invalidation declared on an ancestor, so the invalidated set is the whole document (Style Calculation).

  5. 5
    Layout

    Compute geometry for boxes whose size or position could have changed.

    fails by Read-write-read patterns that force layout several times in one frame (Layout Thrashing).

  6. 6
    Paint record

    Produce display lists for the damaged regions of each layer.

    fails by A large damaged rectangle caused by a shadow, a filter, or a coarse invalidation (Paint Commands).

  7. 7
    Commit + composite

    Hand the layer tree to the compositor, which draws the frame.

    fails by Raster not finished, or so many layers that compositing itself misses the deadline (Layer Explosion).

Everything before "Commit" is the main thread. That is the queue your long task is at the front of.

Work that does not fit

When work genuinely does not fit in a frame, there are four honest responses and no magic one. The choice is a real engineering decision with different costs, and it depends on whether the work is computation, DOM mutation, or simply too much of it.

  • Chunking only helps if the mechanism you use actually yields to input between chunks — a promise chain does not, because microtasks drain before the frame (The Microtask Checkpoint).
  • A worker does not make DOM mutation faster; only the main thread can touch the DOM (Web Workers and the DOM Boundary).
  • The cheapest frame is the one you never asked for. Not animating is always within budget.
This work does not fit in a frame

What kind of work is it, and what can you give up?

Do less work

when The work scales with something you control — rows rendered, components re-rendered, elements invalidated.

cost Usually an architectural change: virtualisation, containment, narrower invalidation (List Virtualization).

Chunk it and yield

when The work is divisible and the user needs the page responsive while it runs.

cost Longer wall-clock time, more complex code, and a partially-updated UI to design for (Yielding and Scheduling).

Move it to a worker

when It is computation over data rather than DOM manipulation — parsing, sorting, diffing, decoding.

cost A serialisation boundary, asynchronous plumbing, and no help at all if the bottleneck was the DOM (When a Worker Is Actually the Answer).

Delegate it to the compositor

when It is animation of transform or opacity on content that is not otherwise changing.

cost Layer memory, and a hard constraint on which properties you may animate (Cheap and Expensive Animation).

Show progress honestly

when The work genuinely cannot be made to fit and the user must wait.

cost A real design problem — and the progress indicator must be driven by actual progress, because a compositor-driven spinner will happily spin through a freeze (Loading, Error, Empty — The States You Did Not Render).

How to build it

Most important first.

  • Budget from the *display*, not from the number you remember. If the product ships on high-refresh phones, the target is the smaller number, and the safe habit is to leave headroom rather than to fill the 60Hz figure.
  • Assume your share of the frame is a minority of it. Reserving roughly half for the browser's own work is a crude rule with the right shape; the precise split is measurable per page and not worth guessing.
  • Do the expensive thing once, not per frame. Read layout before writing, cache measurements, and hoist anything invariant out of the frame callback (Layout Thrashing).
  • Delegate what can be delegated. An animation the compositor runs consumes none of the main-thread budget at all.
  • Break long work into pieces that fit and yield between them, so input can be handled between chunks rather than after all of them (Yielding and Scheduling).
  • Move genuinely heavy computation off the main thread entirely, where the frame budget does not apply to it (When a Worker Is Actually the Answer).
  • Measure frames, not functions. A function that takes a small share of the budget can still cause a dropped frame by invalidating layout for the entire document.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • A dropped-frame scroll or animation is disorienting, and measurably so for users with vestibular sensitivity, attention-related disabilities, or motion sickness. Stutter is not merely unattractive — irregular motion is harder to track than smooth motion or no motion at all.
  • Missing the budget blocks the main thread, and the accessibility tree is updated on the main thread. Focus moves late, announcements queue, and a screen-reader user gets a silence with no explanation (The Accessibility Tree).
  • Honouring prefers-reduced-motion is also a budget strategy: the animation you do not run costs nothing, and for a user who asked for less motion that is the correct outcome twice over.
  • Users on low-end hardware are disproportionately users who cannot choose their hardware. Treating the frame budget as a device-specific constant rather than a fixed one is an accessibility decision as much as a performance one.
  • Never use motion as the only feedback that something happened. If the frame is dropped, the feedback is simply lost; a state change in the DOM survives (Live Regions and Announcement).

What can go wrong

Failure modes
  • One long task blocks several frames. The user sees a freeze, and their clicks during it are all delivered afterwards, at once.
  • Consistent small overruns: every frame takes slightly more than the budget, so the effective rate halves. This looks like "generally a bit choppy" and is harder to find than one dramatic spike.
  • A frame that is late because of raster or compositing while the main thread is idle — invisible to any measurement that only watches your code.
  • Garbage collection landing inside an animation, producing an isolated dropped frame that is not reproducible on demand.
  • The mitigation failing: work split into chunks that are each still too large, or chunks scheduled with a mechanism that does not actually yield to input.
  • Optimising for the 60Hz budget and shipping to a 120Hz device, where the same code drops every other frame.
What can arrive out of order
  • Input arriving mid-task is not delivered until the task completes, so the frame that reflects a click can be several frames after the click.
  • Raster completing after its frame's deadline: the compositor shows the previous tile and the new content appears one frame late.
  • A commit from the main thread landing between compositor frames, so a state change becomes visible one refresh later than the code that made it.
Security
  • The browser does not enforce a frame budget. Nothing stops a page — or an embedded third-party script — from consuming every frame, and a page that hangs the tab is a denial of service the user resolves by leaving (Third-Party Scripts and the Supply Chain).
  • Frame timing is a side channel. High-resolution timers and frame callbacks have been used to infer cross-origin state and to mount microarchitectural attacks, which is why engines coarsen timer resolution and gate the sharper primitives behind cross-origin isolation (Shared Memory and Cross-Origin Isolation).
  • Requestable animation frames stop in background tabs. Anything that depends on them for correctness — a session timer, a queue drain — silently stops when the tab is hidden, and that is a behaviour to design for, not a bug to work around.
Misreads
  • "I have 16ms." You have one over the refresh rate, minus the browser's own work for that frame. On a 120Hz device that is a substantially smaller number than the one usually quoted.
  • "60fps is the target." 60Hz is a common refresh rate and a useful baseline. The target is *the display's* rate, and on much modern hardware that is higher.
  • "My function took less than the budget, so the frame is fine." The frame includes style, layout, paint and composite for everything your function invalidated.
  • "Dropping a frame occasionally is fine." Occasionally, yes. Regularly, no — irregular frame delivery reads as broken software far more than a uniformly lower frame rate does.
  • "If the main thread is idle, frames are on time." Raster and compositing can miss the frame with a completely idle main thread.
  • "This is a web vital I should not hardcode." The frame budget is arithmetic derived from the refresh rate, unlike the loading and responsiveness thresholds, which are published by the vitals working group and do change (Vitals in the Field).

Measuring it, and what changes in the field

How you would see this
  • The frames track in the Performance panel: it shows which frames were produced, which were dropped, and what the browser was doing in each — the only view that covers main thread, raster and compositor together.
  • The FPS meter for a live sense of whether frames are being missed while you interact, and the device's actual refresh rate, which is the denominator of your budget.
  • CPU throttling in devtools to approximate a slower device, which is the closest local proxy for the population you actually ship to (Self Time, Total Time, and Where the CPU Went).
  • Long-task and long-animation-frame reporting in the field, which attributes a slow frame to a script rather than merely recording that it happened (Event-Loop Lag: One Callback, Everybody Waits).
  • Interaction latency from real users, which is where a missed frame budget becomes a number someone else cares about (Interaction Responsiveness).
Slow device, slow network, large data, old tab
  • On a 120Hz phone the budget is roughly half the 60Hz figure, and many current mid-range devices ship exactly that combination: a fast display attached to a slow CPU.
  • On a variable-refresh display the budget moves during the session, and a page can be smooth at one rate and stuttery at another.
  • On a thermally throttled device the CPU slows part-way through a session, so a page that was fine for two minutes starts dropping frames (The Clock Is a Variable).
  • In a background tab, frame callbacks are throttled or stopped entirely, and timers are heavily rate-limited.
  • With a large dataset, per-frame work often scales with what is rendered rather than with what exists — which is the entire argument for virtualisation (List Virtualization).
What this costs
  • Chunking work to fit the budget makes it take longer in total. You are trading throughput for responsiveness, deliberately, and the total-time regression is real.
  • Moving work to a worker costs a serialisation boundary and a more complex data flow, and does not help if the bottleneck was DOM mutation rather than computation (Structured Clone and Transferables).
  • Budgeting for the fastest display means leaving performance unused on slower ones, and building for the slowest device constrains what you can build at all. There is no setting where this is free.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe relationship between refresh rate and frame budget, and the ordering of input, tasks, animation callbacks, style, layout, paint and commit within a frame, are specified behaviour and hold across Blink, Gecko and WebKit.
  • DEVICE-SPECIFICThe budget is one over the display's refresh rate: about 16.7ms at 60Hz, about 11.1ms at 90Hz and about 8.3ms at 120Hz, and variable-refresh displays move between them during a session — so the same code that fits comfortably on a 60Hz desktop monitor can miss every other frame on a 120Hz phone with a slower CPU.
  • BROWSER-SPECIFICThe tooling differs sharply: Chromium's Performance panel has a dedicated frames track with dropped-frame attribution and long-animation-frame reporting, Firefox's profiler exposes frame markers under a different name, and Safari surfaces far less frame-level detail — so a diagnosis built in one browser's frames view has to be re-established in the others.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Computer Architecturefrequency-scalingcpu-vs-gpu
Domains that do not exist yet
  • Programming Languages & Runtime Internals — why garbage collection occasionally lands inside an animation frame, and what the engine does to keep its pauses out of the way.