Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe change adds synchronous-prop classification and per-surface pending-prop tracking. Commit handling carries pending props into shadow trees and retries cancelled flushes. Layout animation proxies merge synchronous props into the light tree and retarget ongoing animations. Native tests cover acknowledgment, versioning, stale writes, and surface removal. The common app adds and registers synchronous-prop and shared-transition examples. Priority: ➖ Normal — Schedule the synchronous-props commit change because it alters Reanimated’s cross-platform commit, registry, and layout-animation behavior across multiple runtime paths. Merge Risk: 🟡 Moderate · up to Conflicting React commits can overwrite synchronous visual values, and disabling synchronous updates does not prevent direct native updates. Both behaviors affect the feature’s core rendering contract and should be corrected before merge. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
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. Comment |
This reverts commit f1075fe. Review found two defects in the overlay: a stale entry could override a newer committed value when a mixed batch routed the synchronous keys through the shadow tree, and the eviction relay took the registry lock and the proxy lock in an order that a custom layout-animation config calling setNativeProps could invert. The next commits fix the erasure at its source instead: commits carry the synchronous values, so the light tree receives correct props without a patch layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89e8edd to
5e57d99
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 9d9fadce-1fc9-4ad0-a2bb-25977a23fdf4
📒 Files selected for processing (23)
apps/common-app/src/apps/reanimated/examples/SharedElementTransitions/AnimatedTransform.tsxapps/common-app/src/apps/reanimated/examples/SharedElementTransitions/LightTreeErasure.tsxapps/common-app/src/apps/reanimated/examples/SynchronousPropsOverwriteExample.tsxapps/common-app/src/apps/reanimated/examples/index.tspackages/react-native-reanimated/CHANGELOG.mdpackages/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpppackages/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.hpackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpppackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.hpackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/SynchronousProps.cpppackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/SynchronousProps.hpackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.cpppackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.hpackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.hpackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyRegistry.cpppackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyRegistry.hpackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpppackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.hpackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsUtils.hpackages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpppackages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.hpackages/react-native-reanimated/android/build.gradle.ktspackages/react-native-reanimated/scripts/reanimated_utils.rb
💤 Files with no reviewable changes (2)
- packages/react-native-reanimated/scripts/reanimated_utils.rb
- packages/react-native-reanimated/android/build.gradle.kts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
The experimental proxy started a LAYOUT animation for every Update on a configured view, even when only props changed. Mirror the legacy proxy: start on a frame change, retarget a running animation otherwise. The retarget rebases the cached animation style onto the new target props, so the final emitted Update carries the change even when no progress callback runs afterwards. The opacity that the restoration inserts into the reused style object is marked, so it never overrides a retargeted value. The next commit attaches synchronous values to commits, which produces exactly such props-only Updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The synchronous path applies props straight to native views, so committed trees lag behind them. Any commit that clones a lagging view then carries stale props: the mounting layer writes them back to the view (visible transform jumps for 8-10 frames), and the layout animations proxy copies them into its light tree, which breaks shared-transition snapshots. Record every applied synchronous batch as pending values in the UpdatesRegistryManager. Commits that happen anyway attach them: Reanimated commits take the pending values of their surfaces, and the commit hook attaches them to non-React-source commits, which clone from trees that never saw the synchronous values. A pending value leaves the store when a Reanimated commit succeeds with it, when settled eviction hands it to React, when its CSS registry gives it up (a registry flush commit carries it first, retried after a commit pause), or when its node unmounts. The write-time light-tree feed stays: it covers the window before the first commit, which no commit can cover because the shared-transition snapshot is read before the same transaction updates the light tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One button moves a box through the synchronous path and resizes it 100 ms later through the shadow path. Without commit-carried synchronous values, the resize commit wrote the stale transform back to the native view for 8-10 frames on iOS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…arries During a state-commit stream (a sticky-header scroll, for example) the commit hook cloned the same pending values onto every commit, although each decorated commit's clones keep the props pointers and therefore already carry them. That cost 40 percent extra app CPU in a profile with 20 never-settling synchronous animations under scripted scrolling. Track the pending-store version in the manager and, per surface, the last root the hook produced. When the next commit builds on that root and the version is unchanged, the base already carries the values and the hook returns the tree untouched. A failed commit never matches (its root never becomes a base), so the hook attaches again - correctness needs no commit acknowledgement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5e57d99 to
58bab99
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
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/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp (1)
417-417: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRetarget active animations after synchronous prop updates.
applySynchronousPropsupdates onlyLightNode::current.props. Active frames uselayoutAnimation.finalViewand cachedUpdateValues.newProps, which can still contain the old props. The next progress frame can therefore overwrite the synchronous native value. UpdatefinalView.propsand rebase the cachedUpdateValues.newPropswith the cloned props, asupdateOngoingAnimationTargetdoes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: fdfd1b33-763e-44ad-984f-3575e717a5eb
📒 Files selected for processing (7)
packages/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpppackages/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.hpackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.hpackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.hpackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpppackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsUtils.hpackages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
Registries flush in order and the pending store merges them per tag and key, so a CSS registry can replace an animated value before the animated registry evicts its older one. The eviction then erased the newer pending value by key alone. Reported by CodeRabbit on #10480. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On Android node->current accumulates the diffed raw props of every Update, while a mutation carries only the latest diff - and Android mounting applies exactly the raw props it receives. Retargeting from the mutation could therefore drop earlier keys from emitted animation frames. The restart path already uses the merged view. Reported by CodeRabbit on #10480. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
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/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp (1)
445-446: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRetarget active animations after synchronous prop updates.
LayoutAnimationsProxy_Experimental::applySynchronousPropsupdatesnode->current.propsbut skipsupdateOngoingAnimationTarget. When an active animation has a matching tag, the nextaddOngoingAnimationspass can use stalefinalViewor cached props and overwrite the synchronous value.node->current.props = getComponentDescriptorForShadowView(node->current) .cloneProps(propsParserContext, node->current.props, RawProps(std::move(rawProps))); + if (layoutAnimations_.contains(shadowNodeFamily->getTag())) { + updateOngoingAnimationTarget(shadowNodeFamily->getTag(), node->current); + }
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: e185163e-e000-4dc1-8cbd-9c42b55756d8
📒 Files selected for processing (3)
packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.cpppackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpppackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.cpp
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 4cbb3698-3ea7-42b6-8fc5-34c3a57d9838
📒 Files selected for processing (19)
packages/react-native-reanimated/CHANGELOG.mdpackages/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpppackages/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedMountHook.cpppackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.cpppackages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.hpackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.hpackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpppackages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.hpackages/react-native-reanimated/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpppackages/react-native-reanimated/__tests__/native/README.mdpackages/react-native-reanimated/__tests__/native/stubs/react/debug/react_native_assert.hpackages/react-native-reanimated/__tests__/native/stubs/reanimated/CSS/InterpolatorRegistry.hpackages/react-native-reanimated/__tests__/native/stubs/reanimated/CSS/registries/StaticPropsRegistry.hpackages/react-native-reanimated/__tests__/native/stubs/reanimated/Fabric/ShadowTreeCloner.hpackages/react-native-reanimated/__tests__/native/stubs/reanimated/Fabric/updates/UpdatesRegistry.hpackages/react-native-reanimated/__tests__/native/stubs/reanimated/Tools/FeatureFlags.hpackages/react-native-reanimated/__tests__/native/synchronousProps.cpppackages/react-native-reanimated/package.jsonpackages/react-native-reanimated/scripts/test-native.mjs
💤 Files with no reviewable changes (1)
- packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
A React commit hook records which pending synchronous keys it covers. The receipt was keyed on the root object. Branch merges, layout-info clones and later commit hooks replace the root, so the receipt never matched and stale pending values came back in later commits. Keep one receipt per pending node, keyed on the Props object the hook created. Props identity survives root clones, state progression and branch merges. Acknowledge receipts against main roots only: the old root of every non-React commit and of the Reanimated transaction, and the mounted root. Handle ReactRevisionMerge as a React commit when synchronous updates are on, so the merged tree gets fresh registry values. A later hook that replaces the Props object of the same node is still not covered.
… can reach a view Every Reanimated commit and every non-React commit cloned all nodes with pending synchronous values. With 40 synchronous transforms and one height animation that commits each frame, app CPU rose 38 percent over the baseline; a sticky header scroll with 20 synchronous rows rose 24 percent. Mounting writes props to a native view only when the props object changed, so an untouched node cannot be reverted by a commit. A Reanimated commit now carries the pending values of the nodes in its batch, of pending nodes below a batch node (a changed ancestor can reparent them), and of views with a layout, entering or exiting animation config, because their shadow props feed animation frames. The commit hook carries a pending node into a non-React commit only when the new root gives it new props, a new place, or an ancestor with new props. Selection runs in two passes so a carried ancestor always brings its pending descendants; the subtree walk has a visit budget with a complete fallback. Clearing on success covers only the carried families. Registry flush commits still carry everything. The light tree keeps a node's props on a layout-only update, since such a mutation carries no new prop values and the light node already holds the synchronous feed. The legacy proxy retargets a running animation and its saved opacity when it forwards a props-only update. Measured on the same scenarios: app CPU +5 percent and +15 percent, with the remaining scroll cost mostly in animation frames outside this change.
…10503) > [!NOTE] > This pull request was authored by AI on behalf of @pawicao. ## Summary Supersedes #10480 with a 23-line change that uses only existing state. With the synchronous prop updates on, a Reanimated commit for a view clones props from a shadow tree that never saw the values the synchronous path applied. On iOS, `RCTViewComponentView` diffs the incoming props against the view's own `_props`, so the commit writes the stale transform back to the layer for several frames. A layout-only Update mutation for the same reason replaced the light-tree props that `applySynchronousProps` had merged, so a shared element transition started from the untransformed position. The fix: 1. `UpdatesRegistryManager::addRegistryProps` appends each registry's current props to the families already in a Reanimated commit. The registry holds a view's latest values until React has them (`UpdatesRegistry::flush` stores the batch before it is partitioned; the settled-props tick erases an entry only after React received it), so no second store is needed. 2. `commitUpdates` calls it once per commit, under the registry lock, before the commit loop, only in the branch that commits a batch. The flush branch already carries every registry value through `collectProps`. 3. `updateLightTree` keeps the light node's props when an Update mutation carries the same Props object. Mounting writes props to a view only when that object changed, so such an Update cannot have changed the native view. No view enters a commit because of this change, no sync-only commit is added, and the commit hook is untouched. Compared with #10480, this drops the pending store, per-key versions, commit receipts, root identity checks, descendant scans, the non-React hook path and the layout-animation retargeting; every one of them rested on a review hypothesis, none on a device reproduction. ## Test plan Set `IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS: true` in `apps/fabric-example/package.json` (`ENABLE_SHARED_ELEMENT_TRANSITIONS` stays on), run `pod install`, build `FabricExample` for the iOS simulator. Overwrite case. Mount this screen and press the button several times, 1-2 s apart. Expected: the box keeps its new position when its height changes. On the base, the box jumps back to the old position for 3-8 frames after each press. ```tsx import { Button, StyleSheet, View } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; export default function Overwrite() { const offset = useSharedValue(0); const height = useSharedValue(100); const transformStyle = useAnimatedStyle(() => ({ transform: [{ translateX: offset.value }] })); const heightStyle = useAnimatedStyle(() => ({ height: height.value })); return ( <View style={{ padding: 20, gap: 20 }}> <Button title="Move, then resize" onPress={() => { offset.value = offset.value === 0 ? 100 : 0; setTimeout(() => { height.value = height.value === 100 ? 150 : 100; }, 100); }} /> <Animated.View style={[{ width: 100, backgroundColor: 'purple' }, transformStyle, heightStyle]} /> </View> ); } ``` Shared transition case. In the `[SET] Light Tree Erasure Repro` example on the base branch, press `shift, erase and go (regression)` on a fresh mount: the transition must start from the green frame. On the base it starts from the red frame in most fresh-mount runs; the runs that pass are the ones where the 500 ms settled-props tick lands inside the 250 ms window before navigation. Measured (matched Debug builds, 30 fps recordings, per-frame pixel analysis): overwrite base 4/4 presses stale, patched 0/4; erasure base 4/6 fresh-mount runs wrong, patched 0/6 (two iterations); sticky header and a React parent-layout change unchanged; Android Pixel 9a clean with the flag off and on. Cost: one extra props parse per family that commits a layout prop in a frame; on 300 such views per frame, `mergeProps` +49% and 3 percentage points more repeated frames on a fixture that is already over budget on the base. ## Changelog - [ ] I added an entry to the `Unpublished` section of each changed package's `CHANGELOG.md`, or this PR does not change `react-native-reanimated` or `react-native-worklets`. No separate entry, as on #10480: the behavior ships under the #10416 entry that this branch is stacked on. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…10503) > [!NOTE] > This pull request was authored by AI on behalf of @pawicao. Supersedes #10480 with a 23-line change that uses only existing state. With the synchronous prop updates on, a Reanimated commit for a view clones props from a shadow tree that never saw the values the synchronous path applied. On iOS, `RCTViewComponentView` diffs the incoming props against the view's own `_props`, so the commit writes the stale transform back to the layer for several frames. A layout-only Update mutation for the same reason replaced the light-tree props that `applySynchronousProps` had merged, so a shared element transition started from the untransformed position. The fix: 1. `UpdatesRegistryManager::addRegistryProps` appends each registry's current props to the families already in a Reanimated commit. The registry holds a view's latest values until React has them (`UpdatesRegistry::flush` stores the batch before it is partitioned; the settled-props tick erases an entry only after React received it), so no second store is needed. 2. `commitUpdates` calls it once per commit, under the registry lock, before the commit loop, only in the branch that commits a batch. The flush branch already carries every registry value through `collectProps`. 3. `updateLightTree` keeps the light node's props when an Update mutation carries the same Props object. Mounting writes props to a view only when that object changed, so such an Update cannot have changed the native view. No view enters a commit because of this change, no sync-only commit is added, and the commit hook is untouched. Compared with #10480, this drops the pending store, per-key versions, commit receipts, root identity checks, descendant scans, the non-React hook path and the layout-animation retargeting; every one of them rested on a review hypothesis, none on a device reproduction. Set `IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS: true` in `apps/fabric-example/package.json` (`ENABLE_SHARED_ELEMENT_TRANSITIONS` stays on), run `pod install`, build `FabricExample` for the iOS simulator. Overwrite case. Mount this screen and press the button several times, 1-2 s apart. Expected: the box keeps its new position when its height changes. On the base, the box jumps back to the old position for 3-8 frames after each press. ```tsx import { Button, StyleSheet, View } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; export default function Overwrite() { const offset = useSharedValue(0); const height = useSharedValue(100); const transformStyle = useAnimatedStyle(() => ({ transform: [{ translateX: offset.value }] })); const heightStyle = useAnimatedStyle(() => ({ height: height.value })); return ( <View style={{ padding: 20, gap: 20 }}> <Button title="Move, then resize" onPress={() => { offset.value = offset.value === 0 ? 100 : 0; setTimeout(() => { height.value = height.value === 100 ? 150 : 100; }, 100); }} /> <Animated.View style={[{ width: 100, backgroundColor: 'purple' }, transformStyle, heightStyle]} /> </View> ); } ``` Shared transition case. In the `[SET] Light Tree Erasure Repro` example on the base branch, press `shift, erase and go (regression)` on a fresh mount: the transition must start from the green frame. On the base it starts from the red frame in most fresh-mount runs; the runs that pass are the ones where the 500 ms settled-props tick lands inside the 250 ms window before navigation. Measured (matched Debug builds, 30 fps recordings, per-frame pixel analysis): overwrite base 4/4 presses stale, patched 0/4; erasure base 4/6 fresh-mount runs wrong, patched 0/6 (two iterations); sticky header and a React parent-layout change unchanged; Android Pixel 9a clean with the flag off and on. Cost: one extra props parse per family that commits a layout prop in a frame; on 300 such views per frame, `mergeProps` +49% and 3 percentage points more repeated frames on a fixture that is already over budget on the base. - [ ] I added an entry to the `Unpublished` section of each changed package's `CHANGELOG.md`, or this PR does not change `react-native-reanimated` or `react-native-worklets`. No separate entry, as on #10480: the behavior ships under the #10416 entry that this branch is stacked on. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Replaces the light tree overlay from #10416 with one rule: commits carry the values the synchronous path applied. This closes the snapshot bug the overlay patched and a second bug that needs no shared transition at all: a later commit wrote a stale transform back to the native view for 8-10 frames (
RCTViewComponentViewdiffs incoming props against the view's own_props, which the synchronous path refreshed).How to read this PR: the first commit reverts the overlay. Every commit after it is the new approach, so
git difffrom the first commit to the head shows the approach against a plain "flags work together" main.How it works
applySynchronousUpdatesrecords each applied batch as pending values in the existingUpdatesRegistryManager, grouped by surface.skipOverlayTags, and the lock-inverting eviction relay are gone.Verified
SynchronousPropsOverwriteExampleshows zero stale-transform frames across four presses (was 8-10 per press); bothLightTreeErasurebuttons start from the green frame on fresh mounts.c5ad53ac4e) passed local checks with minimal React Native stand-ins: a state commit before a branch merge, a state commit before mount,ReactRevisionMerge, a later root clone, concurrent and cancelled attempts, and newer synchronous writes. Compiles on iOS with synchronous updates on and off and on Android with them on; the Android clang-tidy lint passes.56cabb3d0c) went through seven Codex gpt-6-astra review rounds and six independent Claude Fable review rounds until both agreed; it was verified on the iOS simulator with the same frame analysis: the overwrite example, light tree erasure, the sticky header, and the two performance screens.Known limits
enableViewCullingre-creates a culled node from its shadow props, and AndroidenableAccumulatedUpdatesInRawPropsAndroidwrites props on every insert. Both are off by default in 0.87. The carry rules cover reparenting caused by a changed ancestor; culling is not covered.getViewPropandmeasureread committed shadow values, not the values the synchronous path applied. Unchanged by this PR.Commit compatibility
The cleanup keys each receipt on the props object of the pending node, not on the root. See the receipt.
enableFabricCommitBranchingon, React merges a revision through a new root with sourceReactRevisionMerge. The hook handles that source as a React commit when synchronous updates are on, so the merged tree gets fresh registry values. A React-branch commit never acknowledges its old root, because that root can be a staged branch. See React Native's merge and the hook.Animation Backend remains outside the scope of this change.
Performance (baseline vs branch head, Instruments Time Profiler, iOS simulator, debug)
Baseline is
2b0d4b545a, the first commit of this PR, which only lifts the flag exclusion. Sync flag and shared element transitions on in both builds. One 15 s Time Profiler recording per run, two runs per cell, inclusive sample weights, 1 sample is about 1 ms. Both scenarios keep values pending on purpose:withRepeatnever settles. The scenario screens areSynchronousPropsPerfCommitStreamExampleandSynchronousPropsPerfStickyHeaderExample. Neither scenario contains a React commit, so the React receipt path costs nothing here.Scenario A - 40 sync transforms animate while one height animation commits every frame (idle, no interaction):
cloneShadowTreeWithNewPropsgetAncestorsapplySynchronousProps)pullTransactionsynchronouslyUpdateUIPropsThe commit of the height animation now clones only the node it touches. Before the selective carry, the same scenario measured +38 % with 2 s of extra clones.
Scenario B - sticky header with 20 sync-animated rows, 10 scripted swipes (state-commit stream):
shadowTreeWillCommit)cloneShadowTreeWithNewPropsgetAncestorsapplySynchronousProps)pullTransactionsynchronouslyUpdateUIPropsThe header stays pinned and scrolling stays responsive in both builds. State commits no longer clone the rows; the hook cost is now the per-row check of the ancestor chain. The sync apply path and the light tree feed are the same code in both builds; their larger numbers in this run come from more animation frames during the scripted scroll, not from this PR. Before the selective carry, the same scenario measured +24 % with 1.5 s of extra clones and 0.9 s of extra mount diffing.
Traces, per-run JSON and the analysis script are in the local
.tmp/perf3/directory of the measuring worktree, not in the repository.🤖 Generated with Claude Code