mobile/dropped-frames-why-one-slow-function-freezes-a-phone-screen.md

Dropped frames: why one slow function freezes a phone screen

Mobile · 5 min read ·

You tap a button and your phone screen freezes for a moment, even though the app did not crash. The reason is that one thread both listens to your taps and draws every frame, so a slow job leaves it no time to draw.

This article shows how a screen is a flipbook with a budget of about 16 milliseconds per frame, counts the dropped frames with a short Python function, and lets you compare three fixes: the main thread, small chunks and a background thread.

Row of green frame boxes with a run of red boxes in the middle showing dropped frames

You scroll a list on your phone and it glides. Then you tap one button and for a moment everything stops, the list hangs, and a second later it jumps. The app did not crash. It was just busy. This article explains why one slow function can freeze a whole screen, and the three ways apps avoid it.

A screen is a flipbook

A phone screen shows a new picture many times a second. A common screen redraws 60 times per second (60 Hz), and many newer phones do 90 or 120. Each redraw is called a frame. At 60 Hz there are 1000 ÷ 60 ≈ 16.7 milliseconds between frames. At 120 Hz there are only 8.3 ms.

Before each refresh the app must have the next picture ready. If it is ready, the screen shows it. If it is not, the screen shows the previous picture again. That repeated picture is a dropped frame, and your eye reads it as a stutter.

One thread draws everything

A thread is one line of work a program is following, one instruction after another. Both Android and iOS give each app a special thread, usually called the main thread (Android also calls it the UI thread). It does two jobs: it reacts to your taps, and it prepares every frame. It can only do one thing at a time.

So if you put a slow job on the main thread, such as reading a large file, sorting ten thousand items, or decoding a big image, the thread is stuck on that job. It cannot prepare the next frame until the job ends. Every frame whose deadline passes in the meantime is dropped.

Count the dropped frames

Here is the idea as a short Python function. It assumes drawing a frame costs 2 ms, and asks how many screen refreshes pass before the next finished picture is ready.

import math

def dropped_frames(task_ms, hz=60, draw_ms=2):
    frame_ms = 1000 / hz
    ready_at = task_ms + draw_ms
    first_vsync_with_new_picture = math.ceil(ready_at / frame_ms)
    return first_vsync_with_new_picture - 1

for task in [5, 10, 14, 15, 17, 50, 100, 300]:
    print(f"{task:>4} ms task -> {dropped_frames(task):>2} dropped at 60 Hz, "
          f"{dropped_frames(task, hz=120):>2} dropped at 120 Hz")

Running it prints:

   5 ms task ->  0 dropped at 60 Hz,  0 dropped at 120 Hz
  10 ms task ->  0 dropped at 60 Hz,  1 dropped at 120 Hz
  14 ms task ->  0 dropped at 60 Hz,  1 dropped at 120 Hz
  15 ms task ->  1 dropped at 60 Hz,  2 dropped at 120 Hz
  17 ms task ->  1 dropped at 60 Hz,  2 dropped at 120 Hz
  50 ms task ->  3 dropped at 60 Hz,  6 dropped at 120 Hz
 100 ms task ->  6 dropped at 60 Hz, 12 dropped at 120 Hz
 300 ms task -> 18 dropped at 60 Hz, 36 dropped at 120 Hz

Notice two things. A 10 ms task is harmless at 60 Hz but already costs a frame at 120 Hz, because the budget shrank. And the damage grows with the task: 300 ms is not "a bit slow", it is more than a third of a second of a frozen screen. This is a simplified model (real systems queue frames and have extra overhead), but it shows the shape of the problem.

Try it: three ways to run the same job

Below, a ball slides along a track while a heavy job runs. Pick how the job runs, set how long it takes, and press Run. Each box is one screen refresh: green is a new picture, red is a repeated one. The ball only moves when a new picture appears.

The three fixes

1. Move the job to a background thread. A program can run more than one thread at once. Put the slow job on a second thread and the main thread stays free to draw. On Android this is usually done with Kotlin coroutines on a background dispatcher; on iOS, with Swift concurrency or a background queue. When the job finishes, it hands its result back to the main thread, because only the main thread is allowed to change what is on screen.

2. Cut the job into small chunks. If the work cannot leave the main thread, do a little per frame and leave room to draw. Try the middle button above: the job takes slightly longer overall, but the ball never stutters.

3. Do less work. Often the best fix is the boring one: load only the rows on screen, shrink an image before showing it, or cache a result you already computed.

What happens if it gets really bad

A stutter is annoying, but a long freeze can end an app. If an Android app does not respond to a tap or key press for about five seconds, the system can show an "App isn't responding" dialog and offer to close it. That is why file reads and network calls must never wait on the main thread.

What to take from this

When a screen stutters, ask one question: what was the main thread doing during that gap? The budget is about 16 ms at 60 Hz and about 8 ms at 120 Hz, and anything slower than that costs frames. Keep the main thread for tapping and drawing, send the heavy work elsewhere, and your app will feel smooth even when it is busy.

$ ls mobile/

see all →
mobile/list-recycling-why-a-phone-can-scroll-10-000-contacts-while-building-o.md List recycling: why a phone can scroll 10,000 contacts while building only 8 rows A phone can scroll through 10,000 contacts without building 10,000 rows. It builds only enough row views to cover the screen, then recycles… 5 min read · 25 Oct mobile/push-notifications-how-your-phone-buzzes-for-an-app-that-isn-t-running.md Push notifications: how your phone buzzes for an app that isn't running Your phone buzzes with a message from an app that isn't even running. How? Phones keep one shared connection to a push service (Apple's… 4 min read · 22 Oct mobile/offline-sync-what-happens-when-two-phones-edit-the-same-contact.md Offline sync: what happens when two phones edit the same contact You edit a contact's phone number on a plane. Your partner fixes the city on a tablet. Both devices are offline, then both reconnect. Which… 5 min read · 19 Oct mobile/deep-links-why-one-link-opens-an-app-and-another-opens-a-web-page.md Deep links: why one link opens an app and another opens a web page Tap the same link on two phones and one opens an app while the other opens a web page. Who decides, and how? This article explains deep… 4 min read · 16 Oct mobile/app-lifecycle-why-your-phone-forgets-what-you-typed-when-you-switch-ap.md App lifecycle: why your phone forgets what you typed when you switch apps You switch apps for a minute, and when you come back the message you were typing is gone. Nothing was broken: the phone needed memory… 4 min read · 13 Oct mobile/pixels-dp-and-points-why-a-button-of-48-is-tiny-on-one-phone-and-fine.md Pixels, dp and points: why a button of 48 is tiny on one phone and fine on another A button you size at 48 looks fine on one phone and tiny on another, because a sharper screen packs more pixels into the same space. This… 4 min read · 10 Oct