How LazyColumn Works Under the Hood
How LazyColumn Works Under the Hood
LazyColumn renders only the items that are visible on screen. Unlike a regular Column that composes every child upfront, LazyColumn defers composition until the layout phase, composes items on demand as they scroll into view, and disposes them as they scroll out. This lazy behavior is what makes it possible to display lists with thousands of items without running out of memory or dropping frames. But how the runtime achieves this is unusual: it breaks Compose's normal phase ordering by composing new content during the measurement phase, not during the composition phase.
In this article, you'll explore the full rendering pipeline from LazyColumn through LazyLayout to SubcomposeLayout, how items are composed on demand during measurement via subcompose(), the measurement algorithm that determines which items are visible and how they are positioned, the item recycling system that retains up to 7 slots per content type for reuse, the prefetch mechanism that pre-composes items before they become visible, and how scroll position is tracked by key to survive data changes.
The fundamental problem: Composing content you cannot see yet
In a regular Column, every child composable is composed during the composition phase, before measurement begins. If the column contains 10,000 items, all 10,000 are composed, measured, and stored in memory, even though only 10-20 are visible at any time.
LazyColumn solves this by inverting the normal phase order. Instead of composing all items upfront, it composes items during the measurement phase, after it knows the viewport size and scroll position. This means the runtime can calculate which items are visible and compose only those items. Items outside the viewport are never composed at all.
This inversion is possible because of SubcomposeLayout, a special layout composable that exposes a subcompose() function inside the measurement block. Calling subcompose() during measurement triggers composition of the provided content and returns measurables that can be measured and placed in the same pass. This is the foundation that all lazy layouts in Compose are built on.
The layered architecture
LazyColumn is built from four layers, each adding a specific capability:
LazyColumn/LazyRow(public API): accepts aLazyListScopeDSL withitem(),items(), andstickyHeader()functions.LazyList(internal): creates the item provider and measurement policy, applies modifiers for semantics, scroll handling, and animations.LazyLayout(foundation): sets up the content factory, subcompose layout state, and prefetch infrastructure.SubcomposeLayout(ui): enables composition during the measurement phase.
Looking at how LazyList calls LazyLayout:
@Composable
internal fun LazyList(
state: LazyListState,
content: LazyListScope.() -> Unit,
// ... other params
) {
val itemProviderLambda = rememberLazyListItemProviderLambda(state, content)
val measurePolicy = rememberLazyListMeasurePolicy(/* ... */)
LazyLayout(
modifier = modifier
.then(state.remeasurementModifier)
.then(state.awaitLayoutModifier)
.lazyLayoutSemantics(/* ... */)
.then(beyondBoundsModifier)
.lazyLayoutItemAnimator(state.itemAnimator)
.scrollableArea(/* ... */),
prefetchState = state.prefetchState,
measurePolicy = measurePolicy,
itemProvider = itemProviderLambda,
)
}
LazyLayout then wraps SubcomposeLayout with the lazy infrastructure:
@Composable
fun LazyLayout(
itemProvider: () -> LazyLayoutItemProvider,
modifier: Modifier = Modifier,
prefetchState: LazyLayoutPrefetchState? = null,
measurePolicy: LazyLayoutMeasurePolicy,
) {
val currentItemProvider = rememberUpdatedState(itemProvider)
LazySaveableStateHolderProvider { saveableStateHolder ->
val itemContentFactory = remember {
LazyLayoutItemContentFactory(saveableStateHolder) {
currentItemProvider.value()
}
}
val subcomposeLayoutState = remember {
SubcomposeLayoutState(LazyLayoutItemReusePolicy(itemContentFactory))
}
With the content factory and subcomposition state prepared, LazyLayout passes them to SubcomposeLayout along with a measurement lambda. A subcomposition slot is a lightweight composition scope that SubcomposeLayout creates for each item. Each slot holds the composed UI tree for one item and can be reused when a different item of the same content type needs to be displayed:
This article continues for subscribers
Subscribe to Dove Letter for full access to 40+ deep-dive articles about Android and Kotlin development.
Become a Sponsor