Skip to content

Delay adding SharedBounds / SharedElement - #1705

Open
riggaroo wants to merge 3 commits into
mainfrom
feature/delayed-shared-element
Open

Delay adding SharedBounds / SharedElement#1705
riggaroo wants to merge 3 commits into
mainfrom
feature/delayed-shared-element

Conversation

@riggaroo

@riggaroo riggaroo commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Executive Summary

Optimizing lookahead shared element modifiers and Compose runtime CompositionLocal usage resulted in dramatic frame timing improvements across both high-frequency scrolling and navigation transitions:

  • Feed Scrolling: Median CPU frame time dropped by 52.9% (from 10.4 ms down to 4.9 ms), and P99 frame latency dropped by 64.8% (from 45.4 ms down to 16.0 ms), completely eliminating frame drops during scroll flings.
  • Detail Navigation: CPU frame duration at P90 improved from 14.1 ms down to 9.5 ms, and P99 frame overrun improved from +9.9 ms (dropped frames) to -2.2 ms (zero dropped frames throughout the entire forward and reverse transition).

1. Feed Scrolling Benchmark (scrollFeedFrameTiming)

Test Action: Rapid vertical flings down through the feed and back up.

Frame Duration (CPU Time per Frame)

Percentile Baseline (Pre-Optimization) Optimized (Post-Optimization) Absolute Reduction Relative Speedup
P50 (Median) 10.4 ms 4.9 ms -5.5 ms ⚡ 2.12x faster (-52.9%)
P90 17.4 ms 8.6 ms -8.8 ms ⚡ 2.02x faster (-50.6%)
P95 21.9 ms 10.6 ms -11.3 ms ⚡ 2.07x faster (-51.6%)
P99 (Peak) 45.4 ms 16.0 ms -29.4 ms ⚡ 2.84x faster (-64.8%)

Frame Overrun (Frame Deadline Margin @ 60Hz / 16.6ms)

Negative values represent headroom before missing a vsync deadline; positive values represent dropped frames.

Percentile Baseline (Pre-Optimization) Optimized (Post-Optimization) Verdict
P50 -5.8 ms -11.2 ms +5.4 ms additional frame headroom
P90 +1.7 ms (Jank / Dropped) -7.5 ms (Smooth) Dropped frames eliminated
P95 +5.9 ms (Jank / Dropped) -5.4 ms (Smooth) Dropped frames eliminated
P99 +29.7 ms (Severe Jank) +0.3 ms Eliminated ~30 ms jank spikes

2. Snack Detail Navigation Benchmark (navigateSnackDetailFrameTiming)

Test Action: Tap snack card (Forward shared transition) ➔ Snack Detail Screen ➔ Press Back (Reverse shared transition).

Frame Duration (CPU Time per Frame)

Percentile Baseline (Pre-Optimization) Optimized (Post-Optimization) Absolute Reduction Relative Speedup
P50 (Median) 8.0 ms 6.4 ms -1.6 ms ⚡ 1.25x faster (-20.0%)
P90 14.1 ms 9.5 ms -4.6 ms ⚡ 1.48x faster (-32.6%)
P95 16.1 ms 10.4 ms -5.7 ms ⚡ 1.55x faster (-35.4%)
P99 (Peak) 22.3 ms 13.7 ms -8.6 ms ⚡ 1.63x faster (-38.6%)

Frame Overrun (Frame Deadline Margin @ 60Hz / 16.6ms)

Percentile Baseline (Pre-Optimization) Optimized (Post-Optimization) Verdict
P50 -3.3 ms -9.9 ms Smooth throughout
P90 +1.3 ms (Jank / Dropped) -6.7 ms (Smooth) Dropped frames eliminated
P95 +5.0 ms (Jank / Dropped) -5.8 ms (Smooth) Dropped frames eliminated
P99 +9.9 ms (Jank / Dropped) -2.2 ms (Smooth) Zero dropped transition frames

3. Implemented Optimizations

1. On-Demand sharedBounds Attachment via Layout Rect Tracking

  • Problem: Every card in off-screen horizontal LazyRow items unconditionally attached 5 Modifier.sharedBounds lookahead layout nodes (card bounds, background, image, title, tagline). During vertical scroll flings, Compose prefetchers executed expensive lookahead passes for off-screen cards (compose:lazy:prefetch:execute:urgent taking up to 16.1 ms per item).
  • Fix: Added onLayoutRectChanged to cards and dynamically attached sharedBounds only when items enter the visible window (fractionVisibleInWindow() > 0f).
var isVisible by remember { mutableStateOf(false) }

JetsnackCard(
    modifier = modifier
        .onLayoutRectChanged { bounds ->
            val visible = bounds.fractionVisibleInWindow() > 0f
            if (visible != isVisible) {
                isVisible = visible
            }
        }
        .then(if (isVisible) cardSharedBoundsModifier else Modifier)
)

2. staticCompositionLocalOf for Navigation & Shared Transition Scopes

  • Problem: LocalNavAnimatedVisibilityScope and LocalSharedTransitionScope were defined with dynamic compositionLocalOf. Reading .current across dozens of lazy card items registered active snapshot state observers in Compose's slot table.
  • Fix: Converted both scopes to staticCompositionLocalOf, eliminating runtime observer tracking since scope references are invariant within a destination.
val LocalNavAnimatedVisibilityScope = staticCompositionLocalOf<AnimatedVisibilityScope?> { null }
val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope?> { null }

3. Conditional Bypass in JetsnackSurface

  • Problem: JetsnackSurface unconditionally inserted a CompositionLocalProvider(LocalContentColor provides contentColor) subtree for every card, image container, and badge.
  • Fix: Bypassed the provider allocation when contentColor already matches LocalContentColor.current.
if (contentColor != LocalContentColor.current) {
    CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
} else {
    content()
}

4. Scope Reference Hoisting

  • Problem: Calling LocalSharedTransitionScope.current and LocalNavAnimatedVisibilityScope.current inside every leaf item created redundant slot table lookups during rapid item recycling.
  • Fix: Hoisted scope reads to the parent SnackCollection and passed them down as parameters to HighlightedSnacks / Snacks and child items.

@riggaroo
riggaroo marked this pull request as ready for review August 20, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant