configuring-lazy-prefetch — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited configuring-lazy-prefetch (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Lazy layouts pre-compose items just outside the viewport so they're ready when the user scrolls. Compose Foundation 1.9 added LazyLayoutCacheWindow for configurable ahead/behind extents. Compose Foundation 1.10 made prefetch composition pausable by default — work spreads across multiple frames instead of one. Most apps still rely on legacy single-frame prefetch and unknowingly pay for it during heavy scrolling. This skill teaches Claude when (and only when) to widen the window or implement nested prefetch, and how to validate the change with Macrobenchmark.
FrameTimingMetric shows dropped frames at high scroll velocity even though item composables are skippable.LazyColumn rows that each host a HorizontalPager or inner LazyRow).LazyLayoutCacheWindow, NestedPrefetchScope, "prefetch window", "ahead extent", or "pausable composition for prefetch".../optimizing-lazy-layouts/SKILL.md and the underlying stability via ../../stability/diagnosing-compose-stability/SKILL.md.state.value in Composition) — use ../../recomposition/deferring-state-reads/SKILL.md.LazyLayoutCacheWindow API. The cache window is a @ExperimentalFoundationApi factory LazyLayoutCacheWindow(ahead: Dp, behind: Dp) plumbed through rememberLazyListState(cacheWindow = ...) (or the equivalent grid/staggered-grid state). It is NOT a parameter on LazyColumn / LazyRow / LazyVerticalGrid / LazyHorizontalGrid directly. Every call site requires @OptIn(ExperimentalFoundationApi::class).key, contentType, stable item composables. Run ../optimizing-lazy-layouts/SKILL.md first and confirm before tuning the window.FrameTimingMetric. See ../../measurement/generating-baseline-profiles/SKILL.md for the Macrobenchmark scaffold.gradle/libs.versions.toml or the relevant build.gradle.kts, verify androidx.compose.foundation:foundation is at 1.9.0+ (cache window) or 1.10.0+ (pausable prefetch on by default). If on 1.10+ and not measuring jank yet, MUST stop here — the default is good enough for most apps.../optimizing-lazy-layouts/SKILL.md. Every items(...) call has stable key, mixed feeds have contentType, item composables are skippable. Without this baseline, prefetch tuning is treating the symptom.frameDurationCpuMs p50/p95/p99 over a fixed scroll journey so the after-comparison is meaningful. See ../../measurement/generating-baseline-profiles/SKILL.md.LazyLayoutCacheWindow(ahead: Dp, behind: Dp) and plumb it through rememberLazyListState(cacheWindow = window), then pass that state to LazyColumn(state = state) { ... }. The cache window is not a parameter on LazyColumn itself. Every call site needs @OptIn(ExperimentalFoundationApi::class).// RIGHT — measured tall, image-heavy items benefit from a deeper ahead extent
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HeavyFeed(items: ImmutableList<HeavyItem>) {
val state = rememberLazyListState(
cacheWindow = LazyLayoutCacheWindow(ahead = 200.dp, behind = 100.dp)
)
LazyColumn(state = state) {
items(items, key = { it.id }, contentType = { it::class }) { item ->
HeavyItemRow(item, Modifier.animateItem())
}
}
}LazyColumn row contains a HorizontalPager, inner LazyRow, or inner LazyVerticalGrid, the outer prefetch can warm only the row's outer composition by default. Implementing nested prefetch chains the work so the inner layout's first item composes during the outer prefetch slot, not on the user's first horizontal swipe.frameDurationCpuMs p95/p99 before and after. If the rate did not decrease, MUST revert the window change and look elsewhere — likely measure or draw dominates, or item composition was already keeping up.graphicsLayer for alpha/scale) instead.// OK — short, light items: default behavior is correct
LazyColumn {
items(items, key = { it.id }, contentType = { it::class }) { Item(it) }
}// RIGHT — measured-heavy items justify a wider ahead window (Compose Foundation 1.9+)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HeavyFeed(items: ImmutableList<HeavyItem>) {
val state = rememberLazyListState(
cacheWindow = LazyLayoutCacheWindow(ahead = 200.dp, behind = 100.dp)
)
LazyColumn(state = state) {
items(items, key = { it.id }, contentType = { it::class }) { HeavyItem(it) }
}
}// RIGHT — the outer LazyColumn warms the inner pager's first item via nested prefetch
LazyColumn {
items(rows, key = { it.id }, contentType = { it::class }) { row ->
HorizontalPager(
state = rememberPagerState(pageCount = { row.pages.size }),
// Implement NestedPrefetchScope on the inner pager so outer prefetch chains in.
) { pageIndex ->
PagerPage(row.pages[pageIndex])
}
}
}Without nested prefetch, only the outer row warms — the user's first horizontal swipe still pays composition cost for the inner page. With nested prefetch, the inner first page is composed during the outer prefetch slot.
// WRONG
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun WideWindow(items: ImmutableList<Item>) {
val state = rememberLazyListState(
cacheWindow = LazyLayoutCacheWindow(ahead = 2000.dp, behind = 2000.dp)
)
LazyColumn(state = state) { /* ... */ }
}
// WRONG because: a 2000.dp ahead/behind window pre-composes a screenful of extra items on every scroll tick. Memory pressure rises (especially with image-heavy items) and a single direction-reverse wastes most of the prefetched work. Wide windows are not "safe defaults" — they are deliberate trade-offs that MUST be backed by Macrobenchmark numbers.// RIGHT — start narrow, widen only if FrameTimingMetric demands it
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NarrowWindow(items: ImmutableList<Item>) {
val state = rememberLazyListState(
cacheWindow = LazyLayoutCacheWindow(ahead = 200.dp, behind = 100.dp)
)
LazyColumn(state = state) { /* ... */ }
}// WRONG
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Feed(items: List<Item>) { // unstable parameter
val state = rememberLazyListState(
cacheWindow = LazyLayoutCacheWindow(ahead = 400.dp, behind = 200.dp)
)
LazyColumn(state = state) {
items(items, key = { it.id }) { item -> ItemRow(item) }
}
}
// WRONG because: pre-composing additional unstable rows just spreads the same wasted work over more frames. The cache window cannot fix non-skippable item composables.// RIGHT — fix stability first, then leave the window at default; widen only if Macrobenchmark still shows jank
@Composable
fun Feed(items: ImmutableList<Item>) {
LazyColumn {
items(items, key = { it.id }, contentType = { it::class }) { item -> ItemRow(item) }
}
}Cross-reference: ../optimizing-lazy-layouts/SKILL.md and ../../stability/stabilizing-compose-types/SKILL.md.
// RIGHT — on 1.10+, prefetch composition is pausable by default; a single heavy item no longer blows the frame budget
// gradle/libs.versions.toml:
// compose-foundation = "1.10.0"
LazyColumn {
items(items, key = { it.id }, contentType = { it::class }) { HeavyItem(it) }
}Pausable composition lets the prefetch composer suspend mid-item when the frame deadline approaches and resume on the next idle frame. PREFERRED: upgrade to Compose Foundation 1.10+ and benefit from this default before manually widening the cache window.
FrameTimingMetric baseline proves the prefetch window is the bottleneck. Premature tuning increases memory pressure without fixing jank.LazyLayoutCacheWindow (1.9+) or relying on pausable prefetch defaults (1.10+).LazyLayoutCacheWindow(...) with @OptIn(ExperimentalFoundationApi::class). Plumb the window through rememberLazyListState(cacheWindow = ...) (or the matching grid/staggered-grid state factory), then pass that state to the lazy layout — the cache window is not a parameter on LazyColumn / LazyRow / LazyVerticalGrid / LazyHorizontalGrid themselves.Dp (the API takes Dp, not item counts).../optimizing-lazy-layouts/SKILL.md (keys, contentType, item stability) before tuning the cache window. Prefetch cannot fix non-skippable items.NestedPrefetchScope for any item that contains an inner lazy layout (HorizontalPager, inner LazyRow, inner LazyVerticalGrid) before widening the outer window.androidx.compose.foundation:foundation resolves to 1.9.0+ (cache window) or 1.10.0+ (pausable prefetch by default) in the dependency report.FrameTimingMetric frameDurationCpuMs p95 / p99 measurably decreases on the same scroll journey after the change, on a real device with R8 enabled.NestedPrefetchScope was added, the user's first inner-pager swipe runs at native frame rate (no first-swipe stutter).~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.