Building apps that feel fast across all platforms is like cooking the perfect steak – get the timing wrong by just a few seconds and the whole experience suffers. Compose Multiplatform gives us incredible power to share UI code, but without proper lifecycle management and performance tuning, your app might run like a smartphone from 2008.
After optimizing dozens of cross-platform apps, I’ve learned that the secret sauce involves understanding three things: when your UI loads, how it updates, and when it cleans up after itself. Let’s break down how to get this right on every platform.
Lifecycle Events Across Platforms
Each platform handles lifecycles differently. Here’s what matters most:
Platform | Key Moments | Watch Out For |
---|---|---|
Android | onCreate/onStart/onResume | Configuration changes |
iOS | viewDidLoad/viewWillAppear | Background refreshes |
Desktop | window opening/closing | Multiple windows |
Web | DOM mount/unmount | Tab visibility changes |
Here’s how to handle a common scenario – pausing video playback when the app backgrounds:
@Composable
fun VideoPlayer(url: String) {
var isPlaying by remember { mutableStateOf(true) }
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
isPlaying = event.isAtLeast(Lifecycle.State.STARTED)
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
VideoComponent(url, isPlaying)
}
Code language: JavaScript (javascript)
Performance Optimization Techniques
1. Smart List Rendering
Lazy lists are great until they’re not. Here’s how to optimize:
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(messages, key = { it.id }) { message ->
// Content stays stable if message doesn't change
MessageItem(message)
}
}
}
Code language: PHP (php)
Key things to remember:
- Always provide stable keys
- Keep item composables lightweight
- Use
derivedStateOf
for complex transformations
2. Expensive Operations Done Right
Need to process images or do heavy calculations? Here’s the pattern:
@Composable
fun ImageProcessor(input: Image) {
val processedImage by remember(input) {
derivedStateOf {
// This runs on a background thread
withContext(Dispatchers.Default) {
applyFilters(input)
}
}
}
Image(processedImage)
}
Code language: PHP (php)
State Management That Scales
Poor state management is where most performance issues begin. Here’s a comparison of approaches:
Technique | Best For | Performance Impact |
---|---|---|
remember | UI-only state | Minimal |
ViewModel | Screen-level state | Moderate |
StateFlow | App-wide state | Needs careful use |
Redux | Complex apps | Higher overhead |
Here’s how I structure state in production apps:
class ConversationViewModel : ViewModel() {
private val _messages = MutableStateFlow<List<Message>>(emptyList())
val messages: StateFlow<List<Message>> = _messages
fun loadConversation() {
viewModelScope.launch {
_messages.value = repository.loadMessages()
}
}
}
@Composable
fun ConversationScreen(viewModel: ConversationViewModel = viewModel()) {
val messages by viewModel.messages.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadConversation()
}
MessageList(messages)
}
Platform-Specific Optimizations
Each platform has unique optimization opportunities:
Android
- Use
rememberCoroutineScope
for lifecycle-aware coroutines - Implement
onTrimMemory
callbacks
iOS
- Optimize for background app refresh
- Handle memory warnings properly
Desktop
- Manage multiple window states
- Optimize for keyboard/mouse input
Web
- Implement virtual scrolling
- Optimize for browser repaints
Here’s how to handle platform-specific optimizations:
expect fun getPlatformSpecificCacheSize(): Long
@Composable
expect fun PlatformPerformanceMonitor(): Unit
// Android implementation
actual fun getPlatformSpecificCacheSize(): Long {
return Runtime.getRuntime().maxMemory() / 4
}
Code language: JavaScript (javascript)
Debugging Performance Issues
When things get slow, here’s my debugging toolkit:
- Compose Debugger – Identify recomposition counts
- CPU Profiler – Find hot spots
- Memory Analyzer – Catch leaks
- StrictMode – Detect main thread violations
Add this to catch issues early:
@Composable
fun DebugPerformance() {
if (LocalInspectionMode.current) {
SideEffect {
// Debugging code here
}
}
}
Code language: JavaScript (javascript)
Testing Performance
Automated performance tests save headaches later:
@Test
fun messageListPerformance() = runComposeTest {
val testMessages = List(1000) { Message("Test $it") }
setContent {
MessageList(testMessages)
}
onNodeWithTag("MessageList")
.assertIsDisplayed()
.assertRecompositionCount(lessThan(3))
}
Code language: PHP (php)
Final Checklist
Before shipping your app, verify:
- Lifecycle events are handled on all platforms
- Heavy work happens off the main thread
- Lists use keys and stable item types
- State updates are minimal and batched
- Platform-specific optimizations are in place
Remember, good performance isn’t about fancy tricks – it’s about doing the right work at the right time. Get this right, and your users will enjoy an app that feels fast and responsive everywhere.