Moving to the 120 Hz ProMotion display exposed hitches — what is the frame budget, and what fixes them?
After moving the app to a 120 Hz ProMotion device, users report hitches that were invisible at 60 Hz. Below is the Animation Hitches instrument readout for a scroll.
Explain the frame budget at 120 Hz, what the readout shows, and what fixes the hitches.
Animation Hitches
Refresh rate 120 Hz (8.3 ms budget)
Hitch time ratio 14 ms / s (target < 5 ms/s)
Longest hitch 41 ms (commit overran the budget)
Cause (frame) layoutSubviews → image decode on main
Explain what fixes the hitches.
At 120 Hz the frame budget is ~8.3 ms — half of the 16.7 ms at 60 Hz — so work that fit before now overruns and hitches. Animation Hitches reports the hitch-time ratio and longest hitch, here 41 ms from decode inside layoutSubviews. Move decode off-main and cache layout.
- ✗Thinking a higher refresh rate gives more time per frame, not less
- ✗Reading the instrument as average fps instead of the hitch-time ratio
- ✗Assuming the 16.7 ms budget is unchanged at 120 Hz
- →Why does the same code hitch at 120 Hz but not at 60 Hz?
- →What does the hitch-time ratio measure that raw frame rate misses?
A hitch is a frame delivered later than the display needed it. At 60 Hz you had a 16.7 ms budget per frame; at 120 Hz the display asks for a frame every 8.3 ms, so the budget halves. Work that squeaked in under 16.7 ms now overruns 8.3 ms and shows as a hitch — which is why the same build looked fine at 60 Hz and janks on ProMotion.
120 Hz → 8.3 ms budget (was 16.7 ms at 60 Hz)
Hitch time ratio 14 ms/s → well over the <5 ms/s target
Longest hitch 41 ms → ~5 frames missed in one commit
Blamed frame layoutSubviews → image decode on the main thread
The Animation Hitches instrument's key number is the hitch-time ratio (ms of hitch per second of animation), not average fps — an app can average 110 fps and still hitch badly. The readout blames synchronous image decode inside layoutSubviews. Fix it by moving decode off the main thread (downsampled, cached), caching computed layout, and shrinking per-frame main-thread work until each commit fits inside 8.3 ms.