Skip to content

perf: Skip the pre-draw repair on frames without React mounts - #10168

Closed
MatiPl01 wants to merge 2 commits into
@matipl01/android-css-opacity-transitionsfrom
@matipl01/android-css-mount-gated-repair
Closed

MatiPl01 wants to merge 2 commits into
@matipl01/android-css-opacity-transitionsfrom
@matipl01/android-css-mount-gated-repair

Conversation

@MatiPl01

@MatiPl01 MatiPl01 commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

The pre-draw repair re-asserts every running platform transition on every frame, but React can only overwrite an animated value on frames where it wrote props. A UIManagerListener records mounts and the repair returns immediately on all other frames, so the walk over running transitions happens at write rate rather than at the display refresh rate.

That matters most after a transition settles: a persistent hold keeps its entry, so without the gate every later frame the window draws keeps walking the map. Measured on an emulator during a 3s transition, 201 of 240 pre-draw callbacks returned early.

Synchronous updates run their mount item inline (FabricUIManager.synchronouslyUpdateViewOnUIThread), bypassing MountItemDispatcher, so no listener fires for them even though opacity is one of the props they carry. They therefore open the gate explicitly, which makes the invariant true by construction rather than by coincidence. Measured on device, such writes are rare and always landed on a frame where a mount had already opened the gate, so this is insurance rather than an observed fix.

@MatiPl01 MatiPl01 self-assigned this Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 36357854-a0bc-4edc-90f3-23420e58f01d

📥 Commits

Reviewing files that changed from the base of the PR and between 8e2206c and 6957bab.

📒 Files selected for processing (1)
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved animation value recovery after UI updates.
    • Prevented unnecessary visual repairs when no changes occurred since the previous frame, helping maintain smoother rendering.
    • Improved animation stability by applying corrections only when needed.
    • Preserved ongoing animations while reducing redundant rendering work and avoiding visual interruptions.
    • Improved cleanup during animation lifecycle changes to prevent stale UI update tracking.

Walkthrough

The CSS transitions manager tracks Fabric mount completion between draws. It skips clobbered-value repairs when no mount occurred, preserves animator state reporting, and unregisters the listener during cleanup.

Changes

CSS transition repair

Layer / File(s) Summary
Mount tracking and conditional repair
packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt
Registers a UIManagerListener with FabricUIManager. Mount completion sets mountedSinceLastDraw. repairClobberedValues skips repair when no mount occurred, preserves the animator-presence result, and resets the flag before repair. invalidate unregisters the listener.

Estimated code review effort: 2 (Simple) | ~10 minutes

Mergeability Score: ⚪ Minimal · up to 6957b

This localized performance change has no actionable merge-blocking risk remaining after normal checks and review.

Possibly related PRs

Suggested reviewers: tjzel

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: skipping pre-draw repair on frames without React mounts.
Description check ✅ Passed The description directly explains the UIManagerListener gate, early returns, synchronous updates, and the resulting performance improvement.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch @matipl01/android-css-mount-gated-repair

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from ee764f7 to 9ee2ea5 Compare August 10, 2026 10:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt`:
- Around line 32-48: Update CSSPlatformTransitionsManager to retain the
UIManagerListener registered in init, then remove that same listener during
teardown via removeUIManagerEventListener. Ensure NativeProxy.invalidate()
invokes the manager cleanup and clears its reference so the listener is detached
before a new CSSPlatformTransitionsManager is created.
- Around line 40-42: Update
CSSPlatformTransitionsManager.didMountItems(UIManager) so mountedSinceLastDraw
is set only when an actual React mount-item batch was dispatched, not for
command-only callbacks where the mount list is null. Track or consume the
preserved mount-item distinction through the relevant dispatch/listener flow,
while keeping command-only frames from triggering repairClobberedValues().
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6db8df1b-02a0-4dcf-a6d4-353bb6a440ed

📥 Commits

Reviewing files that changed from the base of the PR and between c509641 and 9ee2ea5.

📒 Files selected for processing (1)
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt

@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from c509641 to e1cfb18 Compare August 11, 2026 16:39
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from 9ee2ea5 to 695330c Compare August 11, 2026 16:39
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from e1cfb18 to 76b41d5 Compare August 11, 2026 16:47
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from 695330c to 207677c Compare August 11, 2026 16:47
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from 76b41d5 to 9116dc6 Compare August 11, 2026 23:56
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from 207677c to 73e7ffc Compare August 11, 2026 23:56
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from 9116dc6 to 50b1385 Compare August 12, 2026 07:50
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from 73e7ffc to 181a631 Compare August 12, 2026 07:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt (1)

77-84: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle zero-duration delayed transitions.

durationMs == 0 with a positive delay reaches animateTransition. In this case, delayFraction == 1f, so the final interpolation returns 0f and never writes toValue.

Add an explicit full-delay case and a regression test.

Proposed fix
-        override fun getInterpolation(input: Float): Float =
-            if (input <= delayFraction) 0f else inner.getInterpolation((input - delayFraction) / (1f - delayFraction))
+        override fun getInterpolation(input: Float): Float =
+            when {
+                delayFraction >= 1f -> if (input >= 1f) 1f else 0f
+                input <= delayFraction -> 0f
+                else -> inner.getInterpolation((input - delayFraction) / (1f - delayFraction))
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt`
around lines 77 - 84, Update HoldThenEase and the animateTransition path to
handle durationMs == 0 with a positive delay: ensure the delayed transition
reaches and writes toValue instead of remaining at 0f, while preserving normal
interpolation for nonzero durations. Add a regression test covering a
zero-duration delayed transition and verifying the final value is applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt`:
- Around line 77-84: Update HoldThenEase and the animateTransition path to
handle durationMs == 0 with a positive delay: ensure the delayed transition
reaches and writes toValue instead of remaining at 0f, while preserving normal
interpolation for nonzero durations. Add a regression test covering a
zero-duration delayed transition and verifying the final value is applied.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b132a6d9-bac6-4656-8327-7d99ab80b7c6

📥 Commits

Reviewing files that changed from the base of the PR and between 695330c and 181a631.

📒 Files selected for processing (1)
  • packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/css/CSSPlatformTransitionsManager.kt

@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from 50b1385 to dbdc501 Compare August 12, 2026 08:03
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from 181a631 to 9a8fd6b Compare August 12, 2026 08:03
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from dbdc501 to 90753d5 Compare August 12, 2026 08:11
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from 9a8fd6b to ea1977b Compare August 12, 2026 08:11
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from 90753d5 to 824b5c8 Compare August 12, 2026 08:56
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from ea1977b to 8e2206c Compare August 12, 2026 08:56
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from 824b5c8 to 66fddc4 Compare August 12, 2026 10:58
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch 2 times, most recently from 77866ff to c382b26 Compare August 12, 2026 11:17
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from 24a0080 to 2c79ccd Compare August 12, 2026 11:31
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from c382b26 to 5c31d25 Compare August 12, 2026 11:31
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from 5c31d25 to 051e84d Compare August 12, 2026 11:42
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from 0db10d0 to f930a96 Compare August 12, 2026 12:33
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch 6 times, most recently from de63fcb to 6957bab Compare August 13, 2026 01:21
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from f930a96 to 2a9450d Compare August 13, 2026 01:22
React can overwrite an animated value only on frames where a mount ran, so
a UIManagerListener records mounts and the repair returns immediately on
every other frame instead of walking all running transitions.
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-mount-gated-repair branch from d376c0d to dd32131 Compare August 13, 2026 11:14
@MatiPl01
MatiPl01 force-pushed the @matipl01/android-css-opacity-transitions branch from 2a9450d to 74324f2 Compare August 13, 2026 11:15
@MatiPl01
MatiPl01 deleted the branch @matipl01/android-css-opacity-transitions August 13, 2026 12:15
@MatiPl01 MatiPl01 closed this Aug 13, 2026
MatiPl01 added a commit that referenced this pull request Aug 13, 2026
The pre-draw repair re-asserts every running platform transition on
every frame, but React can only overwrite an animated value on frames
where it wrote props. A `UIManagerListener` records mounts and the
repair returns immediately on all other frames, so the walk over running
transitions happens at write rate rather than at the display refresh
rate.

That matters most after a transition settles: a persistent hold keeps
its entry, so without the gate every later frame the window draws keeps
walking the map. Measured on an emulator during a 3s transition, 201 of
240 pre-draw callbacks returned early.

Synchronous updates run their mount item inline
(`FabricUIManager.synchronouslyUpdateViewOnUIThread`), bypassing
`MountItemDispatcher`, so no listener fires for them even though
`opacity` is one of the props they carry. They therefore open the gate
explicitly, which makes the invariant true by construction rather than
by coincidence. Measured on device, such writes are rare and always
landed on a frame where a mount had already opened the gate, so this is
insurance rather than an observed fix.

Identical in content to #10168, which this replaces. That one was based
on the head branch of #10061, and closing #10061 instead of merging it
left the base pointing at a branch with no live PR, so merging would not
have reached `main`. GitHub refuses to change the base of a pull request
that belongs to a stack, so recreating it was the only way to target
`main`.
@MatiPl01
MatiPl01 deleted the @matipl01/android-css-mount-gated-repair branch August 13, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants