Optimize Jetpack Compose: Performance & Best Practices

 

    Jetpack Compose offers a declarative approach to building UIs in Android. However, like any UI framework, optimization is key to achieving smooth performance and a great user experience. This detailed guide expands on the provided checklist, offering in-depth explanations and practical code examples for each optimization technique.

I. Core Principles: Laying the Foundation for Performance

  • Use Stable Types: Compose relies on stable types to efficiently track changes and minimize recompositions. A stable type guarantees that if two instances of the type are equal according to equals(), they will always be equal. Using unstable types can force unnecessary recompositions.  

// Stable data class (recommended)data class User(val name: String, val age: Int)
// Unstable list (avoid directly in Compose state)
// Use SnapshotStateList instead
val users = remember { mutableStateListOf<User>() }

// Correct usage with SnapshotStateList
var usersState by remember { mutableStateOf(persistentListOf<User>()) }
  • Avoid Nested if Statements: Deeply nested if statements can reduce code readability and potentially impact performance by increasing branching complexity. Favor when statements or extracting logic into separate functions.
// Avoid: Nested ifs
if (condition1) {
if (condition2) {
if (condition3) {
Text("All conditions met")
}
}
}

// Prefer: when statement
when {
condition1 && condition2 && condition3 -> Text("All conditions met")
condition1 && condition2 -> Text("Condition 1 and 2 met")
condition1 -> Text("Condition 1 met")
else -> Text("No conditions met")
}
  • Deferred Reading of States in Compose Phases: Deferring state reads until they’re actually needed for rendering prevents unnecessary recompositions when the state changes but doesn’t affect the current composition. derivedStateOf is crucial for this.
@Composable
fun MyComposable(nameState: State<String>) {
// Defer reading the name until it's used in the Text composable
val displayName by remember { derivedStateOf { "Hello, ${nameState.value}!" } }
Text(displayName)
}
  • Optimize DerivedState and remember Usage: derivedStateOf creates a new state that is derived from one or more other states. It only updates when the source states change. remember is used to cache expensive computations across recompositions.
var count by remember { mutableStateOf(0) }
val isEven by remember { derivedStateOf { count % 2 == 0 } } // Only updates when count changes

val expensiveCalculationResult = remember {
// Perform expensive calculation here
performExpensiveCalculation()
}
  • Pre-Rendering (Not Directly Applicable in Standard Compose): Pre-rendering is more relevant for server-side rendering or static site generation. In standard Compose, focus on optimizing the initial composition.
  • Use Compose Lints for Faster Loading: Use the Compose lint rules provided by Android Studio. These rules can help identify potential performance issues in your code.

II. Composable Function Optimization: Fine-Tuning Composables

  • Mark Functions as @NonRestartableComposable: This annotation tells the Compose compiler that a composable function doesn't need to be restarted during recomposition. Use it for composables that don't read any state or emit any side effects.
@NonRestartableComposable
@Composable
fun StaticText(text: String) {
Text(text)
}
  • Avoid Unstable Lambdas: Creating new lambdas within composables can cause unnecessary recompositions. Use remember to memoize lambdas.
// Avoid: Creating a new lambda on every recomposition
Button(onClick = { /* ... */ }) { Text("Click me") }

// Prefer: Memoizing the lambda
val onClick = remember { { /* ... */ } }
Button(onClick = onClick) { Text("Click me") }
  • Use Lambda-Based State Propagation: Passing lambdas for event handling allows for more granular control over recomposition scopes.
@Composable
fun ParentComposable() {
var count by remember { mutableStateOf(0) }
ChildComposable(onIncrement = { count++ })
Text("Count: $count")
}

@Composable
fun ChildComposable(onIncrement: () -> Unit) {
Button(onClick = onIncrement) { Text("Increment") }
}
  • Material Design and Typography: Using Material Design components and consistent typography contributes to a performant and visually appealing UI.

III. Performance-Specific Optimizations: Addressing Bottlenecks

  • Dumb-UI Shipping (Separation of Concerns): Keep composables focused on presentation and avoid placing business logic within them.
  • Reduce Recomposition Areas: Minimize the scope of recompositions by using appropriate state management and avoiding unnecessary state reads.
  • Minimize Initial Composition Costs: Optimize the initial composition to reduce app startup time.
  • Avoid Long Calculations in UI State Getters: Perform expensive calculations outside of state getters, preferably in background threads or coroutines.
  • Remove Formatting from Compose Screens: Format data before passing it to composables.
  • Avoid Autoboxing in Performance-Critical Code: Use primitive types instead of their boxed counterparts (e.g., int instead of Integer).
  • Provide Stable Key and Content Types (For Lists): When using LazyColumn or LazyRow, provide stable keys to items to help Compose efficiently track changes.
LazyColumn {
items(users, key = { user -> user.id }) { user ->
Text(user.name)
}
}
  • Use Baseline Profiles: Baseline Profiles allow you to precompile parts of your app, improving startup performance.
  • MovableContentOf: This allows you to move composables within the composition tree without recomposition.
  • Defer Composition for Complex Screens: Use rememberSaveable and conditional composition to delay the composition of parts of the UI until they are needed.
  • Avoid Excessive ComposeViews: Minimize the number of ComposeViews in your layouts.
  • Avoid Backward Writes: Avoid modifying state within the composition phase.
  • Use Modifier.Node for Stateful Modifiers: Use Modifier.Node for custom modifiers that need to maintain state.
  • Enable Strong Skipping Mode: This is usually enabled by default in recent Compose compiler versions and improves recomposition performance.

If you reached till here, means you liked the article. Please post your vaulable suggestions in comment box.

Happy Coding 😊

Comments

Popular posts from this blog

JIT vs AOT Compilation | Android Runtime

From ‘Master’ to ‘Main’: The Meaning Behind Git’s Naming Shift