mcpField
C
configuring-lazy-prefetch

configuring-lazy-prefetch

github.com/skydoves开发520129 次安装19 周前更新
代码仓库

关于 configuring-lazy-prefetch

面向 Claude 的技能,指导如何基于测量结果调整 Compose 懒加载布局预取。内容涵盖缓存窗口、嵌套预取和 Macrobenchmark 验证,并强调仅在数据支持时偏离默认设置。

主要功能

  1. 01配置懒加载布局缓存窗口
  2. 02为内层布局使用嵌套预取
  3. 03利用可暂停的预取组合
  4. 04用 Macrobenchmark 测量帧耗时
  5. 05核对 Foundation 版本要求

适用场景

  1. 01排查快速滚动时的丢帧
  2. 02预热懒加载行中的内层 Pager 内容
  3. 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+,用于 LazyLayoutCacheWindow API。缓存窗口是 @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 和真实设备上测量。记录固定滚动流程中的 frameDurationCpuMs p50/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 流程重新测量。 对比更改前后的 frameDurationCpuMs p95/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 的 frameDurationCpuMs p95 / p99 有可测量的下降。
  • Layout Inspector 显示预取项目在空闲帧期间组合(而不是堆积到与用户滚动输入相同的帧中)。
  • 扩大窗口后,内存分析器未显示回归——堆和常驻集大小均保持在预算范围内。
  • 如果添加了 NestedPrefetchScope,用户首次滑动内部 pager 时能以原生帧率运行(没有首次滑动卡顿)。
  • 没有提交将缓存窗口调优与键/contentType/稳定性修复打包在一起——每项更改都应单独发布和测量。

参考资料

评论

支持 Markdown