Skip to content

[Android] Dragging or settling a sheet lays out its React content again on every frame #997

Closed as not planned

Description

@philiplindberg

On Android, while a sheet is dragged or settling, handleSlide calls updateStateDimensionsForPosition(sheetView.top) on every frame. Each new height is a Fabric state update, so React Native lays out the sheet's whole content tree again and mounts UPDATE_LAYOUT items on the main thread. With a long list in the sheet, that's several milliseconds per frame. The settle animation runs on the same thread, at 120 Hz on a Pixel 8, which leaves 8.3 ms per frame, so quick drags and flicks stutter.

Measured with atrace on a Pixel 8: six quick flicks up and down a sheet with detents [0.45, 1] holding a list of about 20 rows.

Per-frame resize (now) Resize once per gesture
Main-thread frames over 8.3 ms 56 20
90th-percentile main-thread frame 6.1 ms 3.1 ms

The remaining slow frames are one as each drag starts and one as each settle ends.

I understand the per-frame height keeps a footer at the visible bottom of the sheet. For sheets without a footer, the content could be as tall as the sheet can get while it moves, and be sized to its detent once it settles, which handleStateSettled already does:

if (interactionState is InteractionState.Dragging || pendingDetentIndex >= 0) {
  if (containerView?.footerView == null) {
    setStateDimensions(getStateWidth(), realScreenHeight - topInset)
  } else {
    updateStateDimensionsForPosition(sheetView.top)
  }
}

That's two layouts per gesture, not one per frame. Content beyond the bottom of the screen isn't visible while the sheet moves, so it looks the same. Would you take this, or would you rather have it behind a prop, or handled another way? Happy to open a PR either way.

Activity

  1. github-actions commented on Oct 29, 2026

    @github-actions

    Hey @philiplindberg! Thanks for opening the issue.

    It looks like your issue is missing a valid reproduction link.

    A minimal reproduction helps us investigate and fix the issue faster. Without one, we may not be able to help.

    Please provide one of the following:

    • A GitHub repository under your username
    • An Expo Snack

    See "How to create a Minimal, Reproducible Example" for more guidance.

    You can edit your original issue or leave a comment with the repro link. Issues without reproductions may be closed after 14 days.

  2. lodev09 commented on Oct 30, 2026

    @lodev09
    Owner

    @philiplindberg this is the whole point of v4, synchronous layout while sheet is being resized so flex works as-is like a regular RN view. If there's a real bug, please create a repro and also feel free to open up a PR for me to test.

  3. philiplindberg commented on Oct 30, 2026

    @philiplindberg
    ContributorAuthor

    Thanks, that makes sense. Here's a reproduction: https://github.com/philiplindberg/true-sheet-repros (screen "#997 Layout on every frame"). Expo SDK 58, React Native 0.88, new architecture, 4.0.0 with no patches. The sheet has detents [0.45, 1] and a FlatList of 20 rows wired up through scrollableRef.

    scripts/measure.ts flicks the sheet up and down 12 times with adb shell input swipe while atrace runs, and reads the app's main-thread Choreographer#doFrame slices for the frames it drew. Pixel 8, Android 17, 120 Hz, release build, three runs each:

    4.0.0 Sized once per gesture without a footer
    Drawn frames that lay out the content 263–273 of 275–285 12 of 301–308
    Main thread per drawn frame, median 3.4–3.9 ms 1.3 ms
    Main thread per drawn frame, 90th percentile 5.0–5.7 ms 2.6–2.9 ms
    Drawn frames over 8.3 ms 10–11 7–11
    Dropped frames 51–55 25–33

    So in a minimal sheet the per-frame layout costs about 2–3 ms of the 8.3 ms budget on every frame of a drag or settle, and about half the dropped frames go with it. The frames over budget don't fall, because the one layout left per gesture takes 10–17 ms. The numbers in the issue came from our app, where the content is heavier. This repro is the one to go by.

    You're right about flex. The repro has a "Show bottom bar" toggle that puts a bar below the list, laid out with flex. With the change from the issue applied, the bar sits below the screen while the sheet is dragged, and only comes back when it settles. So this shouldn't be the default.

    Our case is a list sheet over a map, resting at 0.45, which people flick between that and full height all the time. Its content never depends on the sheet's height, so it pays for the per-frame layout without using it. Would you take it as an opt-in prop, something like layoutWhileDragging={false}, for sheets like this? I can open a PR with whatever name you prefer.

  4. github-actions commented on Oct 30, 2026

    @github-actions

    Thank you for providing a repro! We'll take a look at this issue soon.

  5. lodev09 commented on Oct 30, 2026

    @lodev09
    Owner

    I'm closing this as intended behavior and it is consistent on all platforms. The views are resized natively and JS gets the update normally, I don't see anything wrong with it since this is a supported behavior with yoga. react-native-screens does this as well on their formSheets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions