跪拜 Guibai
← All articles
Android · Android Jetpack · Kotlin

Track Compose Recomposition Causes with an IR-Based Monitoring Plugin

By 稀有猿诉 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Compose performance debugging becomes a compile-time diagnostic: the plugin identifies the exact state that triggers each recomposition, works on every Compose Multiplatform backend, and is disabled in release builds. That spares developers from hand-instrumenting logs or chasing stack traces that never point at the offending state.

Summary

Compose screens lose smoothness when state changes too frequently, triggering excessive recomposition. Pinpointing the problematic state means collecting every state and parameter, diffing values on each recomposition, and logging the changes. Manual instrumentation and tools like Rebugger do this, but they clutter source and require updating monitoring code as signatures evolve.

Two compile-time approaches automate that injection. An ASM-based pipeline for Android injects bytecode that gathers states and diffs them on recomposition, gated behind a feature flag. It relies on fixed bytecode patterns produced by the Compose compiler, so future compiler optimizations can break it, and it only targets Android.

The newer ComposeStateMonitor plugin avoids those limits by hooking into the Kotlin compiler's IR phase. It adds plain @Composable calls before every composable exit, letting the Compose compiler itself process the monitoring code. The plugin adapts to Kotlin 2.0+ versions through compat layers, and it works with JVM, native, wasm, and JS backends. Source and usage are on GitHub.

Takeaways
— Frequent state changes trigger excessive recomposition and are the most common cause of Compose performance problems.
— Declaring values that never change as `val` lets Compose skip recomposition; composable parameter stability is a key factor.
— Tools like Rebugger print recomposition reasons to logcat by comparing state values.
— An ASM-based approach injects monitoring bytecode at build time but is Android-only and can break when Compose compiler bytecode patterns change.
— ComposeStateMonitor uses a Kotlin IR compiler extension to inject logging before every composable exit, and it works across JVM, native, wasm, and JS backends.
— The plugin's design passes plain @Composable calls through the Compose compiler, so it gets the same group/skip logic as user code.
— ComposeStateMonitor adapts to Kotlin 2.0+ via version-specific compat layers.
Conclusions

Moving from ASM bytecode injection to an IR compiler extension shows that Compose Multiplatform tooling now has to hook into the compiler's own pipeline rather than the generated bytecode; the latter is too version-sensitive.

Injecting @Composable calls at the IR level means monitoring code is treated as first-class composable code, getting the same skipping and grouping optimizations, which is a clever way to keep overhead low.

The plugin's need for per-Kotlin-version compat layers underscores how unstable the IR editing API remains even after 2.0, a tax every compiler-plugin author pays.

Concepts & terms
IR compiler extension
A Kotlin compiler plugin that transforms the intermediate representation (IR) after frontend analysis, allowing code injection before backend code generation. It's how Compose itself implements its compiler, and plugins can insert plain @Composable calls.
Compose Multiplatform
Jetpack Compose's cross-platform variant, compiling Kotlin UI code to Android, iOS, desktop, web (wasm), and JS via Kotlin Multiplatform.
ASM
A Java bytecode manipulation library used to instrument compiled .class files; here used for a first-generation Compose monitoring approach.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗