Optimizing Mobile App UI Rendering Speed
On iPhone 13 the app showed 58–60 FPS on most screens, but one screen with custom UICollectionViewLayout consistently dropped to 42–45 FPS on scroll. Instruments showed 14 ms of 16 available was spent on layoutAttributesForElementsInRect — method recalculated all cell positions on each call without caching. This is classic: UI rendering doesn't slow "in general", it slows in specific place for specific reason.
Rendering stutters are one of the most insidious problems because they're visible to users immediately but diagnosed slowly. FPS metric says "bad", but where exactly — needs investigating.
Where Frames Really Get Lost
Main Thread — the Render Enemy
Golden rule — 16 ms per frame (60 FPS) or 8 ms (120 Hz on Pro devices). Everything running on main thread beyond this blocks rendering. Typical culprits:
On iOS: synchronous CoreData work via viewContext right in cellForItemAt, decoding UIImage without preparingForDisplay(), NSAttributedString with size calculation in sizeForItemAt without cache.
On Android: blocking I/O in onBindViewHolder, Bitmap.decodeResource() on main thread, heavy Drawable animations via AnimationDrawable on cheap devices with Mali GPU.
Separately, measure/layout pass problem. On Android Jetpack Compose ConstraintLayout inside LazyColumn with deep nesting triggers two full measure passes per cell. On complex list with 50+ elements noticeable even on Pixel 7.
GPU Overdraw
Overdraw is when one pixel is drawn multiple times per frame. On Android enabled via "Developer Options → Show GPU Overdraw": blue — 1x, green — 2x, pink — 3x, red — 4x+. Red screen on budget Xiaomi with Adreno 610 — guaranteed jank.
Common reason — nested ViewGroup with opaque backgrounds, each layer drawing background over previous. On iOS analog — CALayer with opaque = false where transparency isn't needed, or shouldRasterize without explicit rasterizationScale.
How We Diagnose and Fix
Work starts with Xcode Instruments → Core Animation and Android GPU Inspector or built-in Android Studio Profiler → Rendering. Not guesses — data.
Typical iOS scenario: client complains of "feed stutters". Open Time Profiler, record 5 seconds of scroll. In call tree immediately visible: [SDWebImage sd_setImageWithURL:] eats 8 ms on main thread because someone removed options:SDWebImageAvoidAutoSetImage and images apply synchronously after loading. One flag — FPS jumps from 47 to 59.
Android case with RecyclerView + DiffUtil: developer called submitList() from ViewModel but DiffUtil ran on main thread (used ListAdapter without AsyncListDiffer). On 200-element list diff took ~18 ms. Moving diff calculation to background thread via AsyncListDiffer — problem gone.
Specific Tools and Techniques
iOS:
-
CADisplayLink+ custom FPS monitor in debug build for continuous monitoring -
UIView.setNeedsLayout()vsUIView.layoutIfNeeded()— understanding difference critical in animations -
drawRect:almost always replaced withCALayersublayers — Core Animation renders them on GPU without CPU -
UIGraphicsImageRendererinstead of deprecatedUIGraphicsBeginImageContextWithOptionsfor offscreen rendering - Prefetching via
UICollectionViewDataSourcePrefetching— decode images before cell appears
Android / Compose:
-
Modifier.graphicsLayer {}for hardware-accelerated transforms instead of software -
remember {}andderivedStateOf {}— prevent extra recompositions -
key()inLazyColumn— without it Compose can't reuse nodes when list changes -
Bitmap.Config.RGB_565instead ofARGB_8888where alpha channel isn't needed — half GPU memory
Flutter:
-
RepaintBoundaryaround widgets repainting frequently independently -
constconstructors — widget doesn't recreate when parent rebuilds -
flutter run --profile+ DevTools → Performance overlay — mandatory before release
Case: 120 Hz on iPad Pro
Client made custom animation via UIViewPropertyAnimator with preferredFrameRateRange. Animation ran at 60 FPS instead of 120. Turned out — one CALayer with shouldRasterize = true without explicit rasterizationScale = UIScreen.main.scale * 2. Core Animation limited whole subtree to 60 FPS due to rasterization scale mismatch. After fix animation ran at 120 FPS with noticeable difference in feel.
Work Stages
- Audit — record sessions in Instruments / Android Profiler, collect baseline FPS metrics, janky frames, frame time
- Analysis — identify bottlenecks: main thread blocks, overdraw, extra layout passes
- Fixes — iteratively with measurement after each change
- Regression run — verify fix didn't break neighboring screens
- Monitoring — integrate Firebase Performance or custom FPS monitor for production tracking
Estimate volume after audit — sometimes problem solved in day, sometimes needs rewriting custom layout.
Timeline Guidelines
Point fix (one screen, clear cause) — 1–3 days. Systematic audit and optimization of multiple screens — 1–3 weeks. If problem is architectural (main thread misuse throughout app) — plan 3–6 weeks with phased migration.







