Mobile Development
Rendering Optimization
A Google study from 2016: 53% of mobile users abandon a site if it takes more than 3 seconds to load. But even more critical is a stuttering scroll. An Instagram A/B test showed: improving feed smoothness from 45 to 60 FPS increased session duration by 12%. Users do not think 'the app is lagging' - they simply close it.
- **TikTok** invested significant engineering into feed rendering optimization - video pre-loading, adaptive bitrate, predictive caching of the next 3 videos make feed scrolling responsive even on low-end devices
- **WhatsApp** encountered a performance regression in its chat list when migrating to Jetpack Compose - the fix required revisiting type stability and eliminating unwanted recompositions via @Stable annotations
- **Uber Eats** uses RecyclerView with many cell types (dishes, dividers, ad banners, restaurant blocks) - optimizing DiffUtil and proper ViewType assignment reduced menu rendering time by 40% on list updates
FPS, Jank, and the 16-Millisecond Budget
A phone screen refreshes 60 times per second - that is 16.67 ms per frame. If rendering takes longer, the user sees jank: stuttering animations, a jerky scroll. On 120 Hz displays the budget tightens to 8.33 ms. The main cause of dropped frames is work on the main thread: network requests, JSON parsing, synchronous disk operations. All of them block the UI thread and blow the frame budget.
Android Systrace and Perfetto show the percentage of frames that exceeded their budget. Xcode Instruments -> Core Animation tracks dropped frames on iOS. In Compose, showSystemUiOverlay and the Layout Inspector diagnose unwanted recompositions. The goal: the 99th percentile frame time must stay below 16 ms.
A 60 Hz screen shows jank. What is the maximum frame time that prevents jank?
Layout Passes and Expensive Measurement
Each frame on Android passes through three phases: Measure -> Layout -> Draw. Measure recursively traverses the view tree to compute sizes. If a parent measures a child twice (wrap_content inside wrap_content with weights), this becomes exponential growth. RelativeLayout with cross-dependencies measures children twice per pass. ConstraintLayout solves this in one pass, but only with correctly configured constraints.
In Compose there is no double-measurement problem: each Layout{} can measure each child exactly once. Violating this rule is a compile-time error. IntrinsicSize in Compose is an explicit exception - it is accessed through Modifier.width(IntrinsicSize.Min) and documented as an expensive operation.
Why is ConstraintLayout preferred over nested LinearLayouts for complex UIs?
Overdraw and GPU-Bound Rendering
Overdraw occurs when a single pixel is painted multiple times in one frame. A white Activity background, then a Fragment background on top, then a CardView background, then a TextView background: four layers of paint on one pixel, three of them invisible. The GPU still executes all four draw calls. Android Debug -> Show GPU Overdraw renders a heat map: blue (1x), green (2x), pink (3x), red (4x). The acceptable maximum is 2x overdraw - red should be absent.
Main sources of overdraw: redundant backgrounds at every level of the hierarchy, semi-transparent overlays without clipRect, real-time shadows and blur. Canvas.clipRect() lets the GPU skip invisible regions. Compose automatically does not draw composables outside the viewport (LazyColumn) - manual clipping is not needed.
In Debug mode, Android Show GPU Overdraw shows large red areas. What does this indicate?
Lazy Loading and List Virtualization
RecyclerView on Android, LazyColumn in Compose, UITableView/UICollectionView on iOS - all implement virtualization: only about 20-30 visible cells exist in the hierarchy at any time, regardless of whether the list has 100,000 items. Compose LazyColumn goes further: it not only virtualizes visible items but preserves scroll position state through rememberLazyListState(). Without a proper key in LazyColumn, items are recreated rather than reused.
Flashlist from Shopify for React Native runs up to 10x faster than the standard FlatList: it reuses native cells without recreating JS context. The principle is the same - virtualization - but implemented at the native layer without JS bridge overhead. DiffUtil in RecyclerView computes the minimum set of changes via the Myers diff algorithm.
Lazy loading in lists automatically solves all performance problems - just replace ListView with RecyclerView or FlatList with FlashList
Virtualization solves memory and initial render problems, but does not eliminate jank during scroll - heavy operations in onBindViewHolder or itemContent still block the main thread
Image loading, data parsing, synchronous database reads in the cell binding callback all happen on the main thread during scroll. Solutions: pre-load data in background threads, use placeholder images, async image loading via Coil/Kingfisher
What happens in Compose LazyColumn if no key is provided for items?
Key Ideas
- **16 ms per frame** is a hard budget. The main thread must stay free: network, IO, heavy computation - all go to background threads. This is the root cause of most jank problems
- **Overdraw and extra layout passes** are GPU and CPU problems respectively. Remove redundant backgrounds, use ConstraintLayout instead of nested LinearLayouts, use clipRect for custom views
- **Virtualization + stable keys** - LazyColumn/RecyclerView/UITableView keep only visible cells in memory. Without stable keys, structural list changes trigger full cell recreation
Related Topics
Rendering is tightly coupled to memory management and architectural decisions:
- Memory Management — Bitmap caching, view recycling, and memory leaks directly affect rendering performance - GC pauses cause jank
- Android Architecture Components — ViewModel and LiveData/StateFlow enable correct UI updates without extra renders on screen rotation
Вопросы для размышления
- If LazyColumn shows 60 FPS on a 120 Hz display device - users still see jank. Why, and how would you diagnose it?
- How would you design rendering for a chat with thousands of messages, where each can contain text, image, video, or audio with varying sizes?
- What is the fundamental difference between recomposition in Compose and reconciliation in React? How does this affect optimization techniques?