Chasing frame drops on hardware you did not choose
3 min readPerformanceQA
“Sixty frames per second” is a claim about an average. Stutter is a claim about the worst frames. Those are different measurements, and only one of them is what users feel.
The frame budget is fixed
On a 60 Hz screen you have 16.7 ms per frame. On a 120 Hz ProMotion display you have 8.3 ms. That budget does not scale with how fast the chip is; a faster chip just means more of your work fits inside it.
Which is why a stutter that never appears on the newest phone appears reliably on a three-year-old one. Nothing is wrong with the old device. Your work simply does not fit in its budget.
Read the dips
Take a capture of the interaction that stutters and do three things in order:
- Find where the line drops. Not the average — the drops.
- Note when they happen. Launch? First scroll? Every time an image appears? On a transition?
- Read CPU, GPU and memory at that moment. This is the diagnosis.
| At the dip | You are |
|---|---|
| CPU spikes, GPU flat | CPU-bound on the main thread |
| GPU spikes, CPU flat | Render-bound |
| Memory jumps | Allocating something large mid-frame |
| Network active | Waiting on I/O somewhere it should not be |
| Nothing moves | Blocked outside the process, or throttled |
Four counters and a timestamp resolve most of these in a minute.
The five usual causes
Decoding full-size images
A 4000-pixel photo displayed in a 200-pixel cell still decodes at 4000 pixels. It costs CPU, it costs a large allocation, and if it happens during a scroll it costs frames. Downsample before display, and do it off the main thread.
This is the single most common cause of scroll stutter in list-heavy apps, and the easiest to fix.
Layout work that scales with content
Constraint solving is not linear in view count. A deep hierarchy rebuilt for every cell shows up as CPU that rises with scroll speed — the faster the user scrolls, the worse it gets, which is exactly the opposite of what people expect.
Shadows and blur
Cheap on a desktop GPU, expensive on a phone. A shadow on a moving view without an explicit shadowPath forces the system to compute the shape every frame. Blur is worse: it is an offscreen pass, and stacking several of them is how you become render-bound without writing any expensive code.
Synchronous work in a scroll callback
Anything touching disk, the keychain, or the network during a drag will be visible. UserDefaults reads look free until they are in a callback that fires sixty times a second.
Doing work you are about to throw away
Prefetching six screens ahead, laying out cells that will never be seen, parsing a whole response when the view needs three fields. None of it is wrong in isolation; all of it competes for the same 16.7 ms.
Stutter that never shows in FPS
Some of the worst-feeling problems do not move the frame-rate line at all:
- A tap that takes 300 ms to do anything. The frames were fine; nothing happened in them.
- An animation that starts late. Smooth once it starts, and it started a beat after the finger lifted.
- A transition that drops one frame. Statistically invisible, extremely noticeable.
For these, watch the CPU trace around the interaction and read the log alongside it. A gap in log output during a drag is often the clearest evidence you will get that something blocked.
Testing so the result means something
- Use the oldest device you support. If it is smooth there, it is smooth everywhere.
- Let the device cool first. A throttled capture measures temperature.
- Repeat the same interaction. Same list, same distance, same speed.
- Capture over the cable. Wireless sampling adds jitter you will mistake for the bug.
- Compare two captures, not one. Before and after a change. A single capture is a number; two are a result.
Where to stop
Chasing the last two frames per second on a device three generations old is usually not where the value is. The goal is not a flat line — it is no dip a user notices during the interactions they perform constantly. Fix those, verify on the slowest hardware you support, and move on.
