UI Freezing: Common Causes and Fixes
A frozen UI is worse than a crash — the user does not know if their tap registered, if the app is dead, or if it will come back. UI freezes either resolve (ANR averted) or the OS shows "App not respon
A frozen UI is worse than a crash — the user does not know if their tap registered, if the app is dead, or if it will come back. UI freezes either resolve (ANR averted) or the OS shows "App not responding." Either way, the user experience is broken. This guide covers why UIs freeze and how to fix.
What "frozen" means
The main thread is blocked. The app is not processing input, not redrawing, not responding. In Android terms, the main thread has been busy long enough for the system to be concerned (input events queue up, pending animations stall).
5 seconds main-thread busy → ANR dialog on Android. Similar phenomenon on iOS (Watchdog timeout ~20 seconds for backgrounded apps, shorter for foreground hangs).
Causes
1. Synchronous I/O on main thread
Disk read, network call, database query synchronously on main. Takes variable time; freezes during.
Fix: Never. Always background.
2. Large computation
Image processing, sorting a huge list, JSON parse of a 50MB response.
Fix: Background thread. Coroutines with Dispatchers.Default.
3. Tight loop
While loop waiting for a condition on main thread. Common in beginner code.
Fix: Event-driven. Observe state change; react. No loops.
4. Layout inflation
Complex XML layout with deep hierarchy inflates slowly. Each recomposition starts over.
Fix: Flatten hierarchy. ConstraintLayout preferred. Compose avoids this entirely.
5. RecyclerView / UICollectionView bind cost
onBindViewHolder does heavy work (bitmap decode, JSON parse). Every scroll = freeze.
Fix: Precompute in ViewModel. Keep bind trivial.
6. GC pause
Memory pressure triggers long garbage collection. Pauses the main thread.
Fix: Reduce allocations in hot paths. Use object pools. Kotlin inline for lambdas to avoid allocation.
7. Foreground service start race
Android 12+ startForegroundService must be followed by startForeground() within 5 seconds. Background-restricted apps can fail, causing app to hang awaiting timeout.
Fix: Only start foreground services when app is in foreground or the app has the right to do so.
8. Locked native code
JNI call blocks waiting on a lock held by another thread. Main thread hangs.
Fix: Lock hygiene. Never lock across long operations.
9. Bluetooth / USB enumeration
Device enumeration can take seconds and blocks if done synchronously.
Fix: Async enumeration. Cache.
10. Content provider query
contentResolver.query() on main. Contacts, media store, SMS — all can be slow.
Fix: Coroutines. Paging. Background thread.
Detection
StrictMode (Android)
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()
)
Logs every main-thread violation.
Main-thread checker (iOS Xcode)
Automatically detects UIKit / AppKit calls off main thread.
CPU profiler
Record session. Look for flame graph heights on main thread exceeding 16ms (1 frame).
Systrace / Perfetto
Timeline of main thread activity. Long horizontal bars = freezes.
Fix patterns
Default to background
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetch()
withContext(Dispatchers.Main) {
_state.value = data
}
}
WorkManager for deferred
Background work scheduled by the OS.
Paging 3 library
Loads list items incrementally.
Avoid runBlocking in production code
It defeats coroutines' purpose.
Profile before every release
Catch regression before ship.
How SUSA catches UI freezes
SUSA's performance monitor samples frame time. Screens where frame time exceeds budget (e.g., 20ms p95, or 50ms p99) are flagged as janky. Jank count per screen surfaces problematic areas.
The impatient persona abandons screens that take too long — surfacing the UX impact of slow UI beyond pure metrics.
susatest-agent test myapp.apk --persona impatient --steps 100
Most common in real apps
- Image loading without a library. Devs write custom bitmap decoding. Always slow. Use Glide / Coil / Kingfisher.
- Large JSON parse synchronously. Parse in IO dispatcher, post to main.
- Synchronous analytics flush. Analytics SDKs should never block main. Audit every SDK.
- Database open on main. Room is async-friendly; use it correctly.
- Foreground service startup race. Meet Android 12+ requirements.
UI freezes are solvable. Every perf-focused sprint moves the ANR / hang rate meaningfully. Track the metric; invest consistently.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free