UI Jank Elimination & 60fps Rendering
📖 Concept
UI jank is visible stutter or frame drops caused by frames taking longer than 16.6ms (60fps) or 8.3ms (120fps). Users perceive jank immediately — it makes apps feel sluggish.
Frame budget:
- 60fps display: 16.6ms per frame
- 90fps display: 11.1ms per frame
- 120fps display: 8.3ms per frame
- Frame rendering: UI Thread work + RenderThread work must fit in budget
Common jank causes:
- Overdraw — Drawing the same pixel multiple times (stacked backgrounds)
- Layout complexity — Deeply nested ViewGroups trigger multiple measure passes
- Inflate on demand — Inflating complex layouts during scroll
- Large images — Loading full-resolution images without downsampling
- GC pressure — Allocating objects in onDraw/onBindViewHolder causing GC pauses
- Main thread work — Any computation during scroll
Profiling tools:
- GPU Profiler (on device) — Shows frame rendering time as bars
- Layout Inspector — View hierarchy depth and overdraw
- Systrace / Perfetto — System-wide trace showing exactly where time is spent
- Jetpack Benchmark — Micro and macro benchmarks for measuring performance
Compose-specific jank prevention:
- Mark classes as @Stable or @Immutable for recomposition skipping
- Use derivedStateOf for computed values that shouldn't trigger recomposition
- Defer reads to the Layout/Drawing phase with Modifier.drawWithContent
- Use key() to help Compose identify items in lazy lists
💻 Code Example
1// RecyclerView optimization for smooth scrolling2class OptimizedAdapter(3 private val differ: AsyncListDiffer<Item> = AsyncListDiffer(this, DIFF_CALLBACK)4) : RecyclerView.Adapter<ViewHolder>() {56 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {7 // Use ViewBinding — faster than findViewById8 val binding = ItemViewBinding.inflate(9 LayoutInflater.from(parent.context), parent, false10 )11 return ViewHolder(binding)12 }1314 override fun onBindViewHolder(holder: ViewHolder, position: Int) {15 val item = differ.currentList[position]16 holder.bind(item)17 // ❌ DON'T: allocate objects here (called during scroll)18 // ❌ DON'T: load images without caching19 // ✅ DO: use Coil/Glide with placeholder20 }2122 companion object {23 val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Item>() {24 override fun areItemsTheSame(old: Item, new: Item) = old.id == new.id25 override fun areContentsTheSame(old: Item, new: Item) = old == new26 }27 }28}2930// Compose LazyList optimization31@Composable32fun OptimizedList(items: List<Item>) {33 LazyColumn {34 items(35 items = items,36 key = { it.id } // Stable keys prevent unnecessary recomposition37 ) { item ->38 // Use remember to avoid recomputation39 val formattedDate = remember(item.date) {40 dateFormatter.format(item.date)41 }42 ItemRow(item, formattedDate)43 }44 }45}4647// Stable types for Compose recomposition skipping48@Immutable // Tells Compose this class never changes after creation49data class UiItem(50 val id: String,51 val title: String,52 val imageUrl: String53)5455// derivedStateOf — only recomposes when the derived value changes56@Composable57fun FilteredList(items: List<Item>, query: String) {58 // Without derivedStateOf: recomposes on EVERY items/query change59 // With derivedStateOf: only recomposes when filtered result changes60 val filteredItems by remember(items, query) {61 derivedStateOf {62 if (query.isEmpty()) items63 else items.filter { it.title.contains(query, ignoreCase = true) }64 }65 }66 LazyColumn {67 items(filteredItems, key = { it.id }) { ItemRow(it) }68 }69}
🏋️ Practice Exercise
Practice:
- Enable GPU overdraw visualization and reduce overdraw in one of your layouts
- Profile a LazyColumn with 1000 items using Compose Compiler metrics
- Implement DiffUtil for a RecyclerView and measure the rendering improvement
- Use Perfetto to capture a frame trace and identify the jank cause
- Add @Stable annotations to your Compose data models and verify recomposition skipping
⚠️ Common Mistakes
Allocating objects inside onDraw() or Compose drawing — triggers GC during rendering
Not using DiffUtil/keys in lists — causes full rebind on list updates
Ignoring overdraw — multiple overlapping backgrounds waste GPU cycles
Loading full-resolution images in a list — use thumbnails with Coil/Glide downsampling
Not testing on low-end devices — jank may be invisible on flagship devices but severe on budget phones
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for UI Jank Elimination & 60fps Rendering. Login to unlock this feature.