KeyMob
All articles

Chasing frame drops on hardware you did not choose

3 min readPerformanceQA

“Sixty frames per second” describes an average. Stutter is about the worst frames. These are different measurements, and users feel only the second one.

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. The budget does not change with the speed of the chip. A faster chip simply fits more of your work inside it.

This is why stutter that never appears on the newest phone appears reliably on a three-year-old one. The old device is not faulty. Your work does not fit in its budget.

Read the drops

Take a capture of the interaction that stutters and do three things in order:

  1. Find where the line drops. Look at the drops, not the average.
  2. Note when they happen. At launch? On the first scroll? Each time an image appears? During a transition?
  3. Read CPU, GPU and memory at that moment. This is the diagnosis.
At the drop 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 where you should not be
Nothing moves Blocked outside the process, or throttled

Four counters and a timestamp resolve most of these cases in a minute.

The five common causes

Decoding full-size images

A 4000-pixel photo displayed in a 200-pixel cell still decodes at 4000 pixels. It costs CPU and a large allocation, and during a scroll it costs frames. Downsample before display, and do it off the main thread.

This is the 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 the opposite of what most people expect.

Shadows and blur

Shadows and blur are cheap on a desktop GPU and expensive on a phone. A shadow on a moving view without an explicit shadowPath forces the system to compute its shape every frame. Blur costs more: it requires an offscreen pass, and stacking several can make an app render-bound even when no single piece of code is expensive.

Synchronous work in a scroll callback

Any access to disk, the keychain or the network during a drag is visible. UserDefaults reads seem free until they run in a callback that fires sixty times a second.

Work that is thrown away

Prefetching six screens ahead, laying out cells that are never shown, parsing a whole response when the view needs three fields. None of this is wrong in isolation, but all of it competes for the same 16.7 ms.

Stutter that never shows in FPS

Some of the most noticeable problems do not move the frame-rate line at all:

  • A tap that takes 300 ms to respond. The frames were fine; nothing happened in them.
  • An animation that starts late. It is smooth once it starts, but it starts a beat after the finger lifts.
  • A transition that drops one frame. Statistically invisible, but very noticeable.

For these cases, 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 that something blocked.

Testing so the result is meaningful

  • 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 that is easy to mistake for the bug.
  • Compare two captures, not one. Capture before and after a change. A single capture is a number; two are a result.

Where to stop

Recovering the last two frames per second on a device three generations old is rarely the best use of time. The goal is not a flat line. The goal is to remove the drops users notice in the interactions they perform most often. Fix those, verify on the slowest hardware you support, and move on.