mcpField
C
configuring-lazy-prefetch

configuring-lazy-prefetch

github.com/skydoves開発520129 回インストール19 週間前に更新
リポジトリ

configuring-lazy-prefetch について

Compose の遅延レイアウトで、測定結果に基づいてプリフェッチを調整する方法を Claude に案内するスキルです。キャッシュウィンドウ、ネストしたプリフェッチ、Macrobenchmark による検証を扱い、測定で必要性が確認されない限り既定値を保つ方針です。

主な機能

  1. 01遅延レイアウトのキャッシュウィンドウを設定
  2. 02内側のレイアウトにネストプリフェッチを適用
  3. 03一時停止可能なプリフェッチコンポジションを活用
  4. 04Macrobenchmark でフレーム時間を測定
  5. 05Foundation のバージョン要件を確認

活用シーン

  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 の設定 — キャッシュウィンドウと一時停止可能なコンポジション

Lazy レイアウトでは、ユーザーがスクロールしたときにすぐ表示できるよう、ビューポートのすぐ外側にあるアイテムを事前にコンポーズします。Compose Foundation 1.9 では、先読み・後読みの範囲を設定できる LazyLayoutCacheWindow が追加されました。Compose Foundation 1.10 では、プリフェッチ時のコンポジションがデフォルトで一時停止可能になり、処理が 1 フレームではなく複数のフレームに分散されるようになりました。ほとんどのアプリは、今も従来の 1 フレームで完了するプリフェッチに依存しており、激しいスクロール時にその負荷を知らずに受けています。このスキルでは、キャッシュウィンドウを広げる、またはネストされたプリフェッチを実装するべきタイミング(そしてその場合に限る)と、Macrobenchmark で変更を検証する方法を Claude に教えます。

このスキルを使用するタイミング

  • アイテムのコンポーザブルをスキップ可能にしているにもかかわらず、スクロール速度が高いときに Macrobenchmark の FrameTimingMetric でフレームのドロップが示される。
  • アイテムのコンテンツが重い。たとえば、大きな画像、デコード済みの動画、ネストされたグリッド、測定に時間のかかる処理など。
  • ユーザーが一時的にスクロールして通り過ぎたアイテムのコンポジションを保持したい(例:勢いよくスクロールして戻るジェスチャー)。
  • 外側の Lazy レイアウトに、それぞれ独自の内側の Lazy レイアウトを持つアイテムが含まれる(例:各行に HorizontalPager や内側の LazyRow を持つ LazyColumn)。
  • 開発者が LazyLayoutCacheWindow、NestedPrefetchScope、「prefetch window」、「ahead extent」、「prefetch の pausable composition」について言及している。

このスキルを使用しないタイミング

  • アイテムが軽量で短時間に処理できる。デフォルトの動作で十分なため、先回りしてウィンドウを設定しないでください。
  • アイテムのコンポーザブルがスキップ不可、または不安定。ウィンドウを広げても、同じ無駄がより多くのフレームに分散されるだけです。まず ../optimizing-lazy-layouts/SKILL.md を修正し、../../stability/diagnosing-compose-stability/SKILL.md を使って根本原因となる安定性の問題に対処してください。
  • ボトルネックがコンポジションではなく、アイテムごとのレイアウトや描画にある。Android Studio Profiler/Layout Inspector で確認してください。測定や描画が処理時間の大半を占める場合、プリフェッチの調整では改善しないため、アイテムのコンテンツを調整してください。
  • アイテム内で状態を誤ったフェーズで読み取っている(アニメーションが Composition で state.value を読み取るなど)。../../recomposition/deferring-state-reads/SKILL.md を使用してください。

前提条件

  • LazyLayoutCacheWindow API を使用するには Compose Foundation 1.9 以降が必要です。キャッシュウィンドウは @ExperimentalFoundationApi のファクトリー LazyLayoutCacheWindow(ahead: Dp, behind: Dp) で、rememberLazyListState(cacheWindow = ...)(または同等のグリッド/スタッガードグリッドの状態)を介して渡します。LazyColumn/LazyRow/LazyVerticalGrid/LazyHorizontalGrid に直接渡すパラメーターではありません。呼び出し箇所ごとに @OptIn(ExperimentalFoundationApi::class) が必要です。
  • プリフェッチ時のコンポジションをデフォルトで一時停止可能にするには Compose Foundation 1.10 以降が必要です。1.10 以降では、プリフェッチコンポーザーを一時停止し、フレームをまたいで再開できるため、重いアイテム 1 つによってフレーム予算を超えることがなくなります。
  • アイテム単位の基本的な対策がすでにできていること。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. 一度に変更する変数は1つにします。 キャッシュウィンドウの調整と安定性/キーの修正を同じコミットや PR で組み合わせてはいけません。プロファイリングには原因と結果を切り分けた比較が必要です。2つの変更をまとめると、リグレッションの二分探索ができなくなります。

パターン

パターン:デフォルトと設定済み
// 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+ にアップグレードし、キャッシュウィンドウを手動で広げる前に、このデフォルトのメリットを活用してください。

必須ルール

  • MUST:Macrobenchmark FrameTimingMetric のベースラインによってプリフェッチウィンドウがボトルネックだと確認されるまでは、デフォルトの動作を維持してください。早まった調整はメモリ負荷を高めるだけで、カクつきは解消しません。
  • MUST:LazyLayoutCacheWindow を導入する場合は Compose Foundation の最小バージョン(1.9 以降)を、ポーズ可能なプリフェッチのデフォルト設定に依存する場合はその最小バージョン(1.10 以降)を明記してください。
  • MUST:LazyLayoutCacheWindow(...) を使用するすべての呼び出し箇所に @OptIn(ExperimentalFoundationApi::class) を付与してください。rememberLazyListState(cacheWindow = ...)(または対応するグリッド/スタッガードグリッドの状態ファクトリー)にウィンドウを渡し、その状態を遅延レイアウトに渡してください。キャッシュウィンドウは LazyColumn/LazyRow/LazyVerticalGrid/LazyHorizontalGrid 自体のパラメーターではありません。
  • MUST:先行/後方の範囲は Dp で指定してください(API が受け取るのはアイテム数ではなく Dp です)。
  • MUST:キャッシュウィンドウを調整する前に、../optimizing-lazy-layouts/SKILL.md(キー、contentType、アイテムの安定性)を完了してください。プリフェッチではスキップできないアイテムは修正できません。
  • MUST NOT:キャッシュウィンドウの調整と安定性/キーの修正を同じ PR で組み合わせないでください。一度に変更する変数は 1 つにし、再計測してから繰り返してください。Skydoves の重要な見解その 5:必ずリリースビルド+R8+実機で計測してください。
  • MUST NOT:「念のため」ウィンドウを広げないでください。ウィンドウを広げると、メモリ使用量が増え、スクロール方向が反転した際に無駄な処理が増えます。
  • **推奨:**手動でウィンドウを調整する前に、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 を追加した場合、ユーザーが内側のページャーを初めてスワイプしたときに、ネイティブのフレームレートで動作する(初回スワイプでカクつかない)。
  • キャッシュウィンドウの調整とキー/contentType/安定性の修正をまとめたコミットがない。各変更は個別にリリースして計測する。

参考資料

コメント