An app can be "working" and still feel bad: it scrolls with stutters, takes a while to respond, heats up the phone and drains the battery. Mobile performance is understanding that you have a time budget per frame, finite battery and network resources, and that lists and images are the most common villains. This is the kind of problem a mid-level engineer needs to diagnose and fix.
The 16ms-per-frame budget
For a smooth 60fps animation, the screen is redrawn 60 times per second — meaning you have about 16 milliseconds to prepare each frame. On 120Hz screens, it's ~8ms. If a frame blows this budget, it is "dropped": the image doesn't update in time and the user sees a stutter.
The analogy: it's a production line that must deliver one piece every 16ms. If one station takes too long, the whole line chokes.
What is "jank"
Jank is exactly this visual choking: dropped frames during scrolling or animation, perceived as little stutters. Almost always the cause is heavy work running on the wrong thread at the wrong time.
Off the main/UI thread
The main thread (or UI thread) is the only one that draws the screen and processes touches. If you do anything slow on it — read a large file, parse JSON, decode an image, query the database — it can't draw the next frame and the app freezes. The rule: heavy work goes to background threads (coroutines, GCD/async, isolates, workers); only the result returns to the UI.
// BAD: blocks the UI
let data = downloadAndParseAll() // on the main thread
show(data)
// GOOD
async {
let data = downloadAndParseAll() // in the background
onMainThread { show(data) } // only the result
}
Interviewers often ask you to identify why a screen "freezes while scrolling" — a strong answer points to synchronous work on the main thread.
Long lists: recycling and virtualization
A list of 10,000 items can't create 10,000 views. The solution is to recycle: keep only the visible views (plus a margin) and reuse them as you scroll, swapping only the content. RecyclerView (Android), UICollectionView/diffable (iOS), FlatList (React Native) and ListView.builder (Flutter) do this. Rendering everything at once is the number one cause of jank in lists.
Images: size and cache
- Resize before displaying: loading a 4000px photo into a 100px thumbnail wastes memory and decoding time.
- Memory and disk cache: don't re-download and re-decode the same image on every scroll (libraries like Glide, Coil, SDWebImage, Kingfisher handle this).
- Decode off the main thread: image decoding is expensive.
Battery and network are finite
Every network access and every radio wake-up consumes battery. Good practices:
- Batch requests instead of many small, frequent ones.
- Respect battery saver mode and do heavy work in the background via OS APIs (WorkManager, BackgroundTasks).
- Avoid aggressive polling; prefer push when possible.
- Release sensors (GPS, camera) as soon as they're no longer needed.
Common causes and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| List stutters while scrolling | Work on the main thread / no recycling | Background thread + virtualized list |
| App heats up and drains battery | Polling/loop/GPS always on | Batch work, release sensors |
| Images flicker/slow | No cache, wrong size | Cache + resize |
| Taps are slow to respond | Main thread blocked | Move I/O and parsing to the background |
The main thread is sacred: it should only draw and respond to touches. Everything else — network, database, parsing, images — belongs in the background. Internalize this and most jank disappears.
Conclusion
Mobile performance boils down to respecting the 16ms budget, keeping the main thread free, recycling long lists, handling images with proper size and cache, and spending battery and network as precious resources. Diagnosing where time is being spent, and moving the weight to the right place, is what turns a functional app into one that feels good.