Skip to content

perf: Drive active item position from a single reaction - #586

Merged
MatiPl01 merged 7 commits into
mainfrom
perf/optimize-active-item-position
Jul 5, 2026
Merged

perf: Drive active item position from a single reaction#586
MatiPl01 merged 7 commits into
mainfrom
perf/optimize-active-item-position

Conversation

@MatiPl01

@MatiPl01 MatiPl01 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Fixes the drag-lag regression that appeared after the reanimated 4.5.1 / worklets 0.10.1 / RN 0.86 bump (#584). The library source was unchanged; the bump made a pre-existing O(N)-per-frame cost expensive.

During a drag, activeItemPosition updates every frame, and every item's layout reaction subscribed to it, so dragging one item did work proportional to the whole list each frame. Each item now registers its position mutable and a single reaction in CommonValuesProvider drives only the active item's mutable, so inactive items do no per-frame work.

Also trims related redundant work on the drag and layout path: reused position references for unchanged items, guarded no-op layout and cell-dimension writes, and lighter flex group bookkeeping.

During a drag activeItemPosition updates every frame and each item's layout reaction subscribed to it, so moving one item did O(N) work per frame. Each item now registers its position mutable and a single reaction in CommonValuesProvider drives only the active item's mutable, so inactive items do no per-frame work.

The drag lag appeared after the reanimated 4.5 / worklets 0.10 / RN 0.86 bump; the library source was unchanged. Still needs on-device drag verification.
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
react-native-sortables-docs Ignored Ignored Preview Jul 5, 2026 1:50pm

@MatiPl01 MatiPl01 self-assigned this Jul 4, 2026
MatiPl01 added 3 commits July 4, 2026 17:24
calculateLayout allocates fresh Vector objects for every item on each reorder, so each item's layoutPosition derived value and inactive-item updater fired even when the item did not move. Reuse the previous Vector reference for keys whose value is unchanged so those per-item reactions short-circuit on reference equality, dropping reorder cost to the number of items that actually moved.

Also trims the verbose comments added in the previous commit.
- reconcilePositions returns the previous record when no item moved and no
  key was added or removed, so an identical relayout reuses the same object
  and hits reanimated's === dedup instead of waking every per-item derived.
- Guard the grid overriddenCellDimensions write so an unchanged column width
  no longer wakes every cell's animated style on container resize.
- Move reconcilePositions to utils/layout.ts; keep equality.ts to predicates.
- activeItemPosition now only seeds the initial mutable; live active updates
  flow through the CommonValuesProvider dispatcher.
- Return the runOnUI runner directly from the registration effect cleanup.
- Use the existing getKeyToIndex worklet in the flex insert swap indices
  instead of rebuilding the map with Object.fromEntries.
- Memoize SortableFlex's processed children and wrap it in typedMemo, so it
  no longer re-renders every item on an unrelated parent render (parity with
  SortableGrid).
- Drop the keyToGroup shared value; derive the active item's group by scanning
  the applied layout groups, removing an O(N) map rebuilt on every flex layout.
- Skip the center-origin trigger position write when it is unchanged so a
  stationary finger stops waking the order-updater reaction.
@MatiPl01
MatiPl01 marked this pull request as ready for review July 5, 2026 12:36
MatiPl01 added 3 commits July 5, 2026 15:25
SortableFlex takes its items as `children` (inline JSX), so `children` is a
new reference on every parent render. That makes useMemo(..., [children])
recompute each render and React.memo never skip, so neither guard actually
helps the idiomatic usage. Unlike SortableGrid, which keys its memo off a
stable `data` prop, there is nothing stable to memoize here.
- itemPositions -> itemLayoutPositions (the layout-computed slot each item
  animates toward) and itemPositionValues -> itemRenderPositions (the per-item
  mutables holding the currently rendered position), so the two are easy to
  tell apart. The layout-result `itemPositions` field is left as is.
- Drop the verbose comments added alongside the earlier perf changes; the
  names now carry the intent.
"Current" reads clearer for a per-item mutable that holds where the item is
actually drawn right now (the active item follows the finger; the others
animate toward their itemLayoutPositions slot).
@MatiPl01
MatiPl01 merged commit c498091 into main Jul 5, 2026
11 checks passed
@MatiPl01
MatiPl01 deleted the perf/optimize-active-item-position branch July 5, 2026 14:00
MatiPl01 added a commit that referenced this pull request Jul 7, 2026
)

Occasionally a dragged item would overlap a neighbour and, on drop, two
items settled on the same slot with another slot left empty.

The inactive-item layout reaction skipped re-issuing its `withTiming`
when the new target was within a pixel of the item's *current,
mid-animation* position. A rapid re-order that flipped a displaced
neighbour's target back mid-flight was therefore swallowed while the
item was transiently near that target, and it kept animating to the
wrong slot. #586's `reconcilePositions` stopped handing out fresh
position refs for unchanged items, which removed an accidental
self-correction that had been hiding this, so it surfaced then.

The skip now also requires the layout target itself to be unchanged
since the previous update, read from the reaction's own `prev` value
(the same signal the drop-interpolation branch already uses). It adds no
state and keeps the original sub-pixel `position` check, so it is
strictly a superset of the previous animation triggers and smooth-resize
following is unchanged, while any real re-target now issues a fresh
animation.

Note: the original overlap is timing-dependent and I could not reproduce
it with scripted gestures, so this rests on the code trace rather than a
captured before/after. Verified with typecheck, eslint, all 21 library
tests, and rapid cross-row plus over-and-back re-target drags on device
(every item settles to a distinct slot, no regression).
MatiPl01 pushed a commit that referenced this pull request Jul 23, 2026
# [1.10.0](v1.9.4...v1.10.0) (2026-07-23)

### Bug Fixes

* Add item zIndex preserving based on the order of items ([#534](#534)) ([fb0be4e](fb0be4e)), closes [#36877](https://github.com/MatiPl01/react-native-sortables/issues/36877)
* allow scrolling the ScrollView from draggable items on web ([#574](#574)) ([74879ed](74879ed)), closes [#545](#545)
* Balance onTouchesUp on Sortable.Touchable long press ([#594](#594)) ([f977567](f977567))
* Deduplicate expo example native modules for expo-doctor ([#590](#590)) ([e5af169](e5af169)), closes [#589](#589)
* disable dragging for fixed-order and non-draggable handles ([#598](#598)) ([8ed2e81](8ed2e81))
* dragging stops after a screen is detached and re-attached (gesture-handler v3) ([#573](#573)) ([c70a14f](c70a14f)), closes [#349](#349) [react-native-gesture-handler#3560](https://github.com/react-native-gesture-handler/issues/3560) [#349](#349) [#349](#349)
* Item can settle on another item's slot after a rapid re-order ([#591](#591)) ([9d63817](9d63817)), closes [#586](#586)
* Item position drift on slow web container resize ([#562](#562)) ([8973115](8973115))
* Keep npm version from walking yarn workspaces during release ([#609](#609)) ([0d8d869](0d8d869))
* Keep Sortable.Grid measurements when revealed after react-freeze ([#572](#572)) ([609c73c](609c73c)), closes [#519](#519) [#36877](https://github.com/MatiPl01/react-native-sortables/issues/36877)
* keep Sortable.Touchable onTouchesUp firing on web ([#596](#596)) ([8529fa8](8529fa8)), closes [#594](#594)
* Keep Sortable.Touchable tap and long press working on teleported items ([#602](#602)) ([3efb111](3efb111))
* Keep teleported item in sync with collapsed content ([#595](#595)) ([1fbcf9e](1fbcf9e))
* New-Arch grid mount crash in the example and unrunnable expo example ([#589](#589)) ([01aa1a8](01aa1a8))
* Respect Sortable.Flex alignItems before items are measured ([#560](#560)) ([5c84117](5c84117)), closes [#258](#258)
* Restore tap and long press on teleported items ([#599](#599)) ([e73117b](e73117b))
* Sortable.Touchable crash on gesture-handler v3 (onActivate not a worklet) ([#587](#587)) ([4b3007e](4b3007e))
* Support react-native-gesture-handler 3.x types ([#553](#553)) ([83754e0](83754e0))
* Support react-native-reanimated 4.5 / worklets 0.10 ([#566](#566)) ([828c6d6](828c6d6)), closes [/github.com//issues/552#issuecomment-4634261543](https://github.com//github.com/MatiPl01/react-native-sortables/issues/552/issues/issuecomment-4634261543) [#552](#552)

### Features

* Add react-native-pulsar as a top-priority haptics adapter ([#568](#568)) ([642e734](642e734))
* Auto-detect expo-haptics when available ([#558](#558)) ([a0d7602](a0d7602)), closes [#554](#554)
* Reverse items stacking order  ([#535](#535)) ([14c7d82](14c7d82)), closes [#36877](https://github.com/MatiPl01/react-native-sortables/issues/36877)

### Performance Improvements

* Drive active item position from a single reaction ([#586](#586)) ([c498091](c498091)), closes [#584](#584)
@MatiPl01

Copy link
Copy Markdown
Owner Author

馃帀 This PR is included in version 1.10.0 馃帀

The release is available on:

Your semantic-release bot 馃摝馃殌

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.

1 participant