You've reached for _.throttle a hundred times. You drop it around a scroll handler or a resize listener and the UI stops hitching. You have never had to write one. Over the next seven screens you will — and every wall you hit writing it naively turns out to be something the real implementation already solved. The walls show up in order; the fixes land only after you've felt the need.
Start here. Wire a parallax hero to window.scrollY. The handler reads the current offset and writes a transform — a property read, a property write. Microscopically cheap. You ship it, open the page on a real trackpad, and the hero feels SYRUPY. Not frozen, not broken — thick. A spinner would be a relief; at least then you'd know something was happening. Nothing in the handler looks slow. What you can't see from the code is how often that tiny body runs during a single trackpad swipe: hundreds of times. Each call is a rounding error. The SUM is the wall.
Scroll is bound to a handler. Every pixel of movement is its own call.
No coalescing. No sampling. No "fire at most N times per second." Just pixel, pixel, pixel, pixel.
A trackpad scroll that feels like one smooth swipe produces hundreds of calls under the hood.
Each call reads the offset, computes a transform, and schedules a state update.
Downstream, React diffs. The DOM mutates. None of it was strictly necessary — the user only needed the FINAL position.
In DevTools, the flame chart fills with tiny orange bars back-to-back, one per event.
The main thread is busy running your handler while the compositor waits for a frame to paint.
Dropped frames. Jitter. The parallax hero lags behind the scrollbar.
The handler body is a millisecond. That should be fine. But the handler runs two hundred times per second.
Two hundred milliseconds of work per second of scroll. The frame budget was sixteen. You are twelve frames behind.
Keep dragging — the numbers on the right tell the same story the graph would.
Somewhere in here, the UI-update indicator flips from "live" to "stuttering." That is the wall, live on your screen.
Each scroll pixel fires its own handler — a property read, a small math, a state update. Drag the panel above for a couple of seconds and the event meter climbs into the hundreds. A handler that takes a millisecond each time, fired two hundred times in a second, eats two hundred milliseconds of the same second the browser needed for paint. The budget per frame is sixteen milliseconds. You've spent twelve frames of budget before the browser has had a chance to draw anything else.
And the handler UPDATES STATE. Every call schedules a React re-render. The render tree walks, the reconciler diffs, the commit phase writes DOM. Most of that work goes nowhere — you only needed the FINAL position, or a position SAMPLED every so often, not the position at every intermediate pixel. Your finger is moving continuously; nobody downstream needs continuous updates — a paint can only land once per frame anyway.