Before Strong Skipping Mode became the default, it was widely considered best practice in Compose to prefer immutable collections (e.g., kotlinx.collections.immutable.ImmutableList) over unstable collections like List<T> for stability and better skipping behavior.
With Strong Skipping Mode now being the default, the situation has changed. In most real-world screens, immutable collections don’t automatically outperform unstable ones - in many cases they provide little to no performance benefit, and can even become pure overhead due to conversion/allocation and equality costs.
Case-by-Case Analysis
To make this discussion concrete, I’ll compare ImmutableList<T> and List<T> across three common cases. For simplicity, I’m not considering the case where a List<T> is backe…
Before Strong Skipping Mode became the default, it was widely considered best practice in Compose to prefer immutable collections (e.g., kotlinx.collections.immutable.ImmutableList) over unstable collections like List<T> for stability and better skipping behavior.
With Strong Skipping Mode now being the default, the situation has changed. In most real-world screens, immutable collections don’t automatically outperform unstable ones - in many cases they provide little to no performance benefit, and can even become pure overhead due to conversion/allocation and equality costs.
Case-by-Case Analysis
To make this discussion concrete, I’ll compare ImmutableList<T> and List<T> across three common cases. For simplicity, I’m not considering the case where a List<T> is backed by MutableList<T> and mutated from elsewhere.
The list never changes (always the same instance)
If your list never changes and you keep the exact same instance, List<T> and ImmutableList<T> behave the same: an instance equality (referential equality) check (===) is enough. [1]
The list instance changes only when the content actually changes
This is the "healthy state management" case: the list changes because the data changed, and the UI should recompose.
In this scenario, using ImmutableList<T> adds extra work: you pay an O(N) conversion cost to build it (e.g., toImmutableList()), and you also pay O(N) object equality (structural equality, == or equals()) when Compose compares the new parameter to the previous one. Since you need to recompose anyway, that extra work is pure overhead - and List<T> avoids both costs.
The content doesn’t change, but a new list instance is created frequently
This is the one case where ImmutableList<T> can actually help. If you keep recreating List<T> instances with the same content, Compose will treat the parameter as changed because the instance equality check fails. With ImmutableList<T>, Compose can use object equality to recognize "same content" and skip work higher up the composition tree.
The catch is the cost: equals() for lists is O(N), and if that parameter flows through multiple composable layers, you can end up paying O(N) comparisons repeatedly. If most of the UI work is already isolated in skippable leaf composables, those repeated comparisons (plus conversions) are often harder to justify than just letting List<T> flow down and relying on skipping at the leaf.
So even in this case, ImmutableList<T> isn’t an automatic win.
Conclusion
In short, I usually recommend using List<T> in most cases. This is also true for other unstable collections like Set<T> and Map<K, V>.
Remember: not every composable should be skippable. [2] And not every list needs to be ImmutableList<T>.