关于 configuring-lazy-prefetch
面向 Claude 的技能,指导如何基于测量结果调整 Compose 懒加载布局预取。内容涵盖缓存窗口、嵌套预取和 Macrobenchmark 验证,并强调仅在数据支持时偏离默认设置。
主要功能
- 01配置懒加载布局缓存窗口
- 02为内层布局使用嵌套预取
- 03利用可暂停的预取组合
- 04用 Macrobenchmark 测量帧耗时
- 05核对 Foundation 版本要求
适用场景
- 01排查快速滚动时的丢帧
- 02预热懒加载行中的内层 Pager 内容
- 03验证扩大预取窗口是否有效
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ compose-performance-skills --skill configuring-lazy-prefetch安装后在 Claude Code 中输入/configuring-lazy-prefetch确认已加载。
详细介绍
配置 Lazy Prefetch——缓存窗口与可暂停的组合
惰性布局会预先组合视口外紧邻的项目,以便用户滚动时这些项目已准备就绪。Compose Foundation 1.9 添加了 LazyLayoutCacheWindow,可配置前后扩展范围。Compose Foundation 1.10 默认支持在预取期间暂停组合——工作会分散到多个帧中,而不是集中在一帧内。大多数应用仍依赖传统的单帧预取,并在快速滚动时无意间承受其代价。本技能将教 Claude 何时(且仅在何时)扩大窗口或实现嵌套预取,以及如何使用 Macrobenchmark 验证更改。
何时使用此技能
- Macrobenchmark
FrameTimingMetric显示,即使项目可组合项可跳过,高速滚动时仍会丢帧。 - 项目内容繁重:大型图像、解码后的视频、嵌套网格或开销较大的测量工作。
- 开发者希望保留用户短暂滚过的项目的组合结果(例如,快速滑动后回滚的手势)。
- 外层惰性布局包含带有自身内层惰性布局的项目(例如,每行都包含
HorizontalPager或内层LazyRow的LazyColumn)。 - 开发者提到
LazyLayoutCacheWindow、NestedPrefetchScope、“预取窗口”、“前向扩展范围”或“用于预取的可暂停组合”。
何时不应使用此技能
- 项目开销很小且内容简短;默认行为已足够——不要预先配置窗口。
- 项目可组合项不可跳过/不稳定;扩大窗口只会将相同的浪费分散到更多帧中。首先修复
../optimizing-lazy-layouts/SKILL.md中的问题,并通过../../stability/diagnosing-compose-stability/SKILL.md处理底层稳定性。 - 瓶颈在于每个项目的布局或绘制,而非组合。使用 Android Studio Profiler/Layout Inspector 确认;如果测量或绘制占主导,调整预取无法解决问题——应改为调整项目内容。
- 项目内部在错误阶段读取状态(动画在组合阶段读取
state.value)——请使用../../recomposition/deferring-state-reads/SKILL.md。
先决条件
- Compose Foundation 1.9+,用于
LazyLayoutCacheWindowAPI。缓存窗口是@ExperimentalFoundationApi工厂LazyLayoutCacheWindow(ahead: Dp, behind: Dp),通过rememberLazyListState(cacheWindow = ...)(或对应的网格/交错网格状态)传入。它不是LazyColumn/LazyRow/LazyVerticalGrid/LazyHorizontalGrid直接提供的参数。每个调用点都需要@OptIn(ExperimentalFoundationApi::class)。 - Compose Foundation 1.10+,用于默认在预取期间支持可暂停的组合。在 1.10+ 中,预取组合器可以跨帧暂停和恢复,因此单个繁重项目不再会超出帧预算。
- 项目级规范已落实:
key、contentType和稳定的项目可组合项。请先运行../optimizing-lazy-layouts/SKILL.md并确认无误,再调整窗口。 - 已设置用于
FrameTimingMetric的 Macrobenchmark 模块。Macrobenchmark 脚手架请参阅../../measurement/generating-baseline-profiles/SKILL.md。
工作流
-
1. 确认 Compose Foundation 版本。 在
gradle/libs.versions.toml或相关的build.gradle.kts中,确认androidx.compose.foundation:foundation为 1.9.0+(缓存窗口)或 1.10.0+(默认启用可暂停预取)。如果版本为 1.10+,且尚未测量卡顿,必须在此处停止——默认设置对大多数应用来说已经足够。 -
2. 再次确认已落实条目级修复。 打开
../optimizing-lazy-layouts/SKILL.md。每处items(...)调用都有稳定的key,混合信息流有contentType,条目可组合项可跳过。没有这个基线,调整预取只是在治标。 -
3. 更改任何内容前,使用 Macrobenchmark
FrameTimingMetric测量基线。 Skydoves 的热门观点 #5:调试构建会骗人。务必在发布构建、启用 R8 和真实设备上测量。记录固定滚动流程中的frameDurationCpuMsp50/p95/p99,以便进行有意义的前后对比。参见../../measurement/generating-baseline-profiles/SKILL.md。 -
4. 只有测量结果证明有必要时,才构造
LazyLayoutCacheWindow。 使用顶层工厂LazyLayoutCacheWindow(ahead: Dp, behind: Dp)构造窗口,通过rememberLazyListState(cacheWindow = window)传入,再将该状态传给LazyColumn(state = state) { ... }。缓存窗口不是LazyColumn本身的参数。每个调用点都需要@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())
}
}
}
-
5. 为包含内部惰性布局的条目实现
NestedPrefetchScope。 当LazyColumn行包含HorizontalPager、内部LazyRow或内部LazyVerticalGrid时,默认情况下,外部预取只能预热该行的外部组合。实现嵌套预取可以串联这些工作,让内部布局的第一个条目在外部预取时段内完成组合,而不是等到用户第一次水平滑动时才组合。 -
6. 使用相同的 Macrobenchmark 流程重新测量。 对比更改前后的
frameDurationCpuMsp95/p99。如果指标没有下降,必须撤销窗口更改并另寻原因——瓶颈可能在测量或绘制,也可能是条目组合原本就能跟上。 -
7. 如果在 Compose Foundation 1.10+ 且启用可暂停预取时仍然丢帧,请分析以确认瓶颈。 使用 Android Studio CPU 分析器 / 系统跟踪,确定每帧中组合、测量或绘制哪个阶段占主导。预取调整只能解决组合开销;对于测量或绘制开销,应简化条目内容(使用更小的图像、更少的子组合、用
graphicsLayer调整透明度 / 缩放)。 -
8. 一次只更改一个变量。 不得在同一个提交或 PR 中同时调整缓存窗口和稳定性 / 键修复。分析需要隔离的因果对比;捆绑两项更改会导致无法通过二分法定位回归原因。
模式
模式:默认设置与配置设置
// 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])
}
}
}
没有嵌套预取时,只会预热外部行——用户第一次水平滑动时,仍需承担内部页面的组合开销。启用嵌套预取后,内部的第一个页面会在外部预取时段内完成组合。
模式:反模式——“以防万一”设置较宽的窗口
// 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) }
}
}
交叉参阅:../optimizing-lazy-layouts/SKILL.md 和 ../../stability/stabilizing-compose-types/SKILL.md。
模式:依赖可暂停组合(Compose Foundation 1.10+)
// 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) }
}
可暂停组合会让预取组合器在帧时限临近时暂停当前条目的组合,并在下一个空闲帧恢复。**推荐:**升级到 Compose Foundation 1.10+,先利用这一默认特性,再手动扩大缓存窗口。
强制规则
- 必须保持默认行为,除非 Macrobenchmark
FrameTimingMetric基准测试证明预取窗口是瓶颈。过早调优会增加内存压力,却无法解决卡顿。 - 必须在引入
LazyLayoutCacheWindow(Compose Foundation 1.9+)或依赖可暂停预取默认行为(1.10+)时,说明 Compose Foundation 的最低版本。 - 必须为所有使用
LazyLayoutCacheWindow(...)的调用点添加@OptIn(ExperimentalFoundationApi::class)注解。通过rememberLazyListState(cacheWindow = ...)(或对应的网格/交错网格状态工厂)传入窗口,然后将该状态传给惰性布局——缓存窗口不是LazyColumn/LazyRow/LazyVerticalGrid/LazyHorizontalGrid本身的参数。 - 必须以
Dp表示向前/向后的范围(该 API 接受Dp,而非项目数量)。 - 必须先完成
../optimizing-lazy-layouts/SKILL.md(键、contentType、项目稳定性),再调优缓存窗口。预取无法解决不可跳过的项目。 - 不得在同一个 PR 中同时调优缓存窗口并修复稳定性/键问题——一次只改变一个变量,重新测量,然后继续迭代。Skydoves 的直言 #5:始终在启用 R8 的发布版本和真实设备上进行测量。
- 不得为了「保险」而扩大窗口。窗口越大,内存占用越多,反向滚动时浪费的工作也越多。
- **首选:**升级到 Compose Foundation 1.10+,默认启用可暂停预取,然后再手动调优窗口。
- **首选:**在扩大外层窗口之前,为任何包含内部惰性布局(HorizontalPager、内部 LazyRow、内部 LazyVerticalGrid)的项目实现
NestedPrefetchScope。
验证
- 依赖报告显示
androidx.compose.foundation:foundation解析到 1.9.0+(缓存窗口)或 1.10.0+(默认启用可暂停预取)。 - 在真实设备上启用 R8,并沿相同的滚动路径进行测试;更改后,Macrobenchmark
FrameTimingMetric的frameDurationCpuMsp95 / p99 有可测量的下降。 - Layout Inspector 显示预取项目在空闲帧期间组合(而不是堆积到与用户滚动输入相同的帧中)。
- 扩大窗口后,内存分析器未显示回归——堆和常驻集大小均保持在预算范围内。
- 如果添加了
NestedPrefetchScope,用户首次滑动内部 pager 时能以原生帧率运行(没有首次滑动卡顿)。 - 没有提交将缓存窗口调优与键/
contentType/稳定性修复打包在一起——每项更改都应单独发布和测量。
参考资料
- Android Developers — 列表和网格:https://developer.android.com/develop/ui/compose/lists
- Android Developers — 性能概述:https://developer.android.com/develop/ui/compose/performance
- Android Developers — Jetpack Compose 新特性(2025 年 5 月,1.9):https://android-developers.googleblog.com/2025/05/whats-new-in-jetpack-compose.html
- Android Developers — Jetpack Compose 新特性(2025 年 12 月,1.10):https://android-developers.googleblog.com/2025/12/whats-new-in-jetpack-compose-december.html
- Android Developers — Macrobenchmark FrameTimingMetric:https://developer.android.com/topic/performance/baselineprofiles/measure-baselineprofile
- Ben Trengrove — 为什么应该始终在发布版本中测试 Compose 性能:https://medium.com/androiddevelopers/why-should-you-always-test-compose-performance-in-release-4168dd0f2c71
- skydoves — compose-performance 资源中心:https://github.com/skydoves/compose-performance