Finding Dropped Frames in Frame Rendering Stats
A dropped frame is the moment the compositor reaches a display refresh with nothing new to present, so it re-shows the previous image and your animation visibly stutters β the miss originates in the frame production phase, when main-thread work overruns the vsync deadline. This guide is the counting-and-attribution companion to DevTools Performance Profiling, part of Rendering Performance Metrics and Tooling: the parent walks the whole capture-to-fix workflow, while this page focuses narrowly on the Frame Rendering Stats overlay and the Performance panelβs Frames track, so you can prove a specific frame was dropped and name the task that dropped it.
The overlay answers βhow many frames am I losing, and when,β in real time and without a recorded trace. The Frames track answers βwhich task caused this one.β You need both β a live counter to catch the regression, and a timestamped record to attribute it β and this walkthrough runs them together.
How a Frame Gets Dropped
The browser produces frames on a fixed cadence driven by the displayβs refresh signal, usually every 16.6ms on a 60Hz panel. On each BeginFrame signal, the compositor thread asks the main thread to run any requestAnimationFrame callbacks and then flush style, layout, and paint. The main thread commits the result back to the compositor, the raster workers turn painted display lists into GPU tiles, and the compositor draws the new tiles and swaps them onto the screen. Every one of those steps has to finish inside a single refresh interval for the frame to be presented on time.
When the main-thread portion overruns β a long rAF callback, a forced synchronous layout, a heavy style recalc β the commit misses its slot. The compositor arrives at the next vsync with no fresh commit and simply re-presents the last drawn frame. That re-presentation is the dropped frame. Critically, the drop is a scheduling failure, not a rendering error: the pixels are correct, they are just late, which is why the symptom is stutter rather than corruption. The compositorβs own draw path can keep up while the main thread starves it.
Here is a minimal reproduction. A scroll-driven rAF loop sorts and reflows a large list on every frame, and the sort plus the geometry read together exceed the budget:
const list = document.querySelector('#feed')
function onFrame() {
const items = [...list.children]
// β heavy work every frame: O(n log n) sort + forced layout read
items.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)
items.forEach((el) => list.appendChild(el)) // re-append re-dirties layout each iteration
requestAnimationFrame(onFrame)
}
requestAnimationFrame(onFrame)
With a few hundred rows this callback runs well past 16.6ms, so roughly every other BeginFrame finds the main thread still busy and the compositor drops a frame.
Reading the Frame Rendering Stats Overlay
Open the Rendering drawer (Command Menu β βShow Renderingβ) and enable Frame Rendering Stats. Chrome paints a small overlay in the top-left of the viewport with three live readouts: the current and running-average frames per second, the GPU memory in use, and β the part that matters here β a scrolling bar graph where each bar is one frameβs presentation state. Presented-on-time frames draw as short green bars; dropped or partially presented frames draw as taller red bars. A steady wall of green with occasional red spikes is a localized jank; a graph that is more red than green is a sustained overrun, exactly what the reproduction above produces.
The overlayβs value is that it is live and cheap. You do not have to record and reload a trace to know whether a fix helped β you interact with the page, watch the ratio of green to red change under your hands, and get an immediate qualitative signal. Treat it as the smoke alarm, not the forensic report: it tells you a frame was dropped and roughly when, but not which function was responsible.
The overlayβs per-frame ratio maps directly onto the compositor thread and rasterization path: when raster or the compositor draw itself is the bottleneck rather than the main thread, you still see red here, which is your cue to check whether the work could move off the main thread entirely.
Attributing the Overrun to a Task
Once the overlay confirms drops are happening, record a Performance trace to attribute them. The Frames track at the top of the flame chart shows one tile per produced frame. Hover any tile and DevTools labels it precisely: βFrameβ for an on-time frame, βDropped Frameβ for one the compositor never presented, and βPartially Presented Frameβ for one where the compositor drew but a main-thread update missed the commit. A dropped frame renders as a red, diagonally-hatched tile β line it up vertically with the main-thread track directly below it and you are looking at the exact task that stole the budget.
The annotated trace below shows the reproduction under 4x CPU throttling. The dropped frame in the Frames track sits directly above a long Animation Frame Fired task whose self-time is dominated by getBoundingClientRect, the forced-layout culprit:
[Frames] ββββ ...
ββ Frame 118 β presented (14.9ms)
ββ Frame 119 β DROPPED (hatched red) β align down β
ββ Frame 120 β Partially Presented
[Main] βββββββββββββββββββββββββββββββββββββββββββββ
Task 33.7ms βΈ Long task ribbon
ββ Animation Frame Fired onFrame @ feed.js:6 (31.2ms)
ββ Recalculate Style (5.1ms)
ββ Layout (18.4ms) β Forced reflow
β ββ get boundingClientRect @ feed.js:9 β attributed read
ββ Layout (repeat) (6.9ms) β append re-dirtied tree
Budget 16.6ms | Task 33.7ms β 1 frame produced across ~2 vsyncs = 1 dropped
Two numbers close the case. The task self-time (33.7ms) is more than double the 16.6ms budget, and the Frames track shows one presented frame where two should have appeared. That is the definition of a dropped frame expressed as a ratio: work-per-frame divided by budget rounds up to the number of vsyncs consumed, and every extra vsync is a drop. When the offending read is getBoundingClientRect, offsetTop, or scrollTop inside the loop, this is the same forced-synchronous-layout signature covered in Finding Layout Thrashing in DevTools.
The Fix
The overrun has two causes: the O(n log n) sort runs every frame, and the geometry read forces a synchronous layout inside the loop. The fix does the sort only when the data actually changes, batches every read before any write so layout flushes once, and reorders with a DocumentFragment so the live tree is not re-dirtied per item.
// AFTER β work is decoupled from the frame loop and reads are batched
const list = document.querySelector('#feed')
let orderDirty = true
// mark work only when the underlying data changes, not every frame
function onDataChanged() { orderDirty = true }
function onFrame() {
if (orderDirty) {
const items = [...list.children]
// read phase: one batched layout pass, no interleaved writes
const tops = new Map(items.map((el) => [el, el.offsetTop])) // single forced layout, amortized
items.sort((a, b) => tops.get(a) - tops.get(b))
// write phase: build offscreen, attach once β no per-item reflow
const frag = document.createDocumentFragment()
items.forEach((el) => frag.appendChild(el))
list.appendChild(frag) // one layout invalidation total
orderDirty = false
}
requestAnimationFrame(onFrame)
}
requestAnimationFrame(onFrame)
The read/write split is the general remedy for the forced-layout half of the problem, covered in depth in Batch DOM Reads and Writes to Stop Thrashing. Re-run the overlay after the change: the bar graph should turn solid green during scroll, and a fresh Frames track should show consecutive βFrameβ tiles with no red hatching.
For interactions rather than continuous animation, the same dropped-frame math surfaces as input latency β a click whose handler overruns the budget delays the next presented frame, which is why Measuring INP with the Event Timing API and Tracking Long Animation Frames catch in the field what the Frames track shows in the lab.
Verification Checklist
Frequently Asked Questions
What is the difference between a dropped frame and a partially presented frame?
A Dropped Frame means the compositor reached vsync with no fresh commit at all and re-presented the previous image. A Partially Presented Frame means the compositor drew and presented on time, but a main-thread update (a rAF change or layout result) missed that frameβs commit and only appears one refresh later. Both show as red-hatched tiles in the Frames track; hover to read the exact label. Partial presentation usually points at a main-thread commit that lands just after the compositor deadline rather than a total overrun.
Why does the Frame Rendering Stats overlay show red frames when my Performance trace looks fast?
The overlay counts presentation, not just main-thread duration. If your main-thread tasks are short but raster or GPU work is slow β large paint areas, too many compositor layers, or exhausted GPU memory β frames still miss the deadline. Check the GPU readout in the overlay and the compositor/raster tracks in the trace. The bottleneck may live on the compositor thread, not in your JavaScript.
Does capping the display or animation at 30fps eliminate dropped frames?
It changes the target, not the mechanism. On a 60Hz display a 30fps animation is meant to present a new frame every second vsync; if a task still overruns that doubled budget you drop frames against the 30fps target instead. The overlayβs target readout adjusts to the refresh rate, but the fix is always to get per-frame work under whichever budget the display cadence sets, not to lower the target.
Can I count dropped frames programmatically instead of by eye?
Yes. requestAnimationFrame gives you each frameβs high-resolution timestamp, so a delta larger than roughly 1.5 refresh intervals indicates at least one skipped frame. For a lower-overhead signal, the Long Animation Frames API reports frames that blocked for over 50ms directly. Both are field-friendly complements to the DevTools overlay, which only runs while the panel is open.
Related Guides
- DevTools Performance Profiling β the full capture-to-fix workflow this page plugs into.
- Finding Layout Thrashing in DevTools β attribute the forced-reflow that most often overruns a frame.
- Tracking Long Animation Frames β catch the same overruns in the field without DevTools open.
- Compositor Thread and Rasterization β where frames are drawn when the main thread is not the bottleneck.
- Batch DOM Reads and Writes to Stop Thrashing β the read/write split that gets per-frame work under budget.