Skip to content

perf: eliminate cascading re-renders with SV-based cell positioning#234

Open
LunatiqueCoder wants to merge 13 commits into
LOCAL-28/v2from
LOCAL-34/draxlist_react_state
Open

perf: eliminate cascading re-renders with SV-based cell positioning#234
LunatiqueCoder wants to merge 13 commits into
LOCAL-28/v2from
LOCAL-34/draxlist_react_state

Conversation

@LunatiqueCoder
Copy link
Copy Markdown
Member

Summary

  • SV-based positioning: RecycledCell uses left:0/top:0 + stacked translateX/Y from basePositionSV + shiftSV — zero Yoga relayout on position changes
  • Per-cell subscription: CellBindingStore + useSyncExternalStore — only recycled cells re-render, position changes flow through SharedValues with zero React overhead
  • Stacked transforms fix: base position and shift as separate transform entries (not combined) — prevents Reanimated spring reset on worklet re-evaluation
  • _contentPosition on DraxView: bypasses stale view.measure() for transform-positioned list cells (timing gap between SV writes and UI-thread transform application)
  • Hover render chain: direct JS→JS forceRender — eliminates the old hoverTriggerSV → useAnimatedReaction → scheduleOnRN bounce
  • O(log N) slot detection: cumulativeEndsSV enables binary search for single-column lists (skips frozenBoundariesSV write entirely)
  • Binding refresh after commitReorder: fixes stale dataIndex causing wrong item content after reorder

Test plan

  • Reorderable list (5000 items): fast scroll → stop → grab → items stay visible
  • Reorder: drag item, drop → correct item at drop position, no duplicates
  • Cross-container drag → no regression
  • Grid layout → no regression
  • npx tsc --noEmit passes

🤖 Generated with Claude Code

LunatiqueCoder and others added 12 commits March 27, 2026 10:08
- Add flex:1 to inner wrapper View so tile content fills cells vertically
- Use itemDimensionsRef height in visibility check for multi-row items
- Grow totalContentSize during drag reorder to prevent item clipping
- Add testIDs to mixed-grid example tiles

Fixes #217 (reported: packGrid overflow clipping items after reorder)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fillStyle (flex:1) should only apply when getItemSpan AND numColumns > 1,
matching how computeGridPositions uses packGrid. Prevents layout issues
if getItemSpan is passed to a single-column list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- packGrid returns cellOwners flat array for O(1) cell→item lookup
- Virtual slot algorithm: pack grid WITHOUT dragged item at drag start
  (gap layout). Finger → gap cell → item key → insertion index.
  Gap layout frozen for entire drag = zero oscillation.
- Snap target accounts for ScrollView offset within padded DraxView
- Slot detection subtracts scrollContainerOffset for correct cell mapping
- freezeSlotBoundaries tracks dragged key to avoid stale gap layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ayout

Add flexWrap mode to DraxList for reordering variable-width items (tags,
chips, pills). Uses packFlex greedy packing with virtual slot detection
(frozen gap boundaries) for stable reorder without oscillation.

- packFlex utility for left-to-right row-wrapping layout
- Nearest-by-distance slot detection on frozen gap boundaries
- Worklet path gated off for flex-wrap (JS-thread slot detection only)
- scrollContainerOffsetRef for accurate snap targets with padding
- New example: sortable-flex.tsx with 24 variable-width tag chips

Closes #158, closes #111.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Migrate all runOnJS → scheduleOnRN (non-curried API, 8 calls)
- Migrate all runOnUI → scheduleOnUI (non-curried API, 9 calls)
- Eliminate DraxProvider re-renders: hoverVersion state → hoverTriggerSV SharedValue
  HoverLayer watches via useAnimatedReaction and forces its own local re-render
- Eliminate redundant SV.value reads in buildDraggedViewData (3 per call):
  pass startPosition, grabOffset as params from worklet instead of reading SVs
- Cache spatialIndexSV/scrollOffsetsSV reads in buildReceiverViewData callers
- Pass grabOffset from gesture worklet to handleReceiverChange
- Drop indicator: left/top → translateX/translateY (GPU fast path, no layout passes)
- Auto-scroll: cache scrollPosition.value (11 → 1 cross-thread sync per tick)
- Pre-flatten hover styles at view registration (avoids 5 StyleSheet.flatten at drag start)
- Move FlattenedHoverStyles type from HoverLayer to types.ts (needed by ViewRegistryEntry)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Items flashed back to original positions for one frame after reorder
because shifts were cleared (SV write → immediate UI thread) before
cells received new baseX/baseY props (deferred to forceRender).

Fix: recompute base positions eagerly during the render phase (in the
data sync block) instead of in useLayoutEffect. Cells now get new
base positions in the SAME React commit as the shift clear — no gap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

commitReorder now commits data internally (dataRef + keyToIndexRef),
making the library self-sufficient. When the parent echoes back the
same array reference from onReorder, the data sync detects it via
awaitingEchoRef and skips the expensive forceRender + updateVisibleCells
cycle — eliminating one full React render on every reorder commit.

Also removes all console.log statements from production code paths
(DraxList, useSortableList) to reduce JS thread overhead during drag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Atomic scheduleOnUI in syncRefsToWorklet: all SV writes land in one
  UI-thread batch. isDraggingSV set LAST to gate worklet — prevents
  stale-data slot detection that caused items to jump after reorder.

- Pre-sync large SharedValues during render/measurement: basePositionsSV,
  itemHeightsSV, orderedKeysSV written in useLayoutEffect and callbacks
  (not drag start). syncRefsToWorklet drops from O(N) to O(K) visible cells.

- Permanent shifts on echo: when parent echoes back reordered data,
  skip ALL work (no recomputeBasePositions, no clearShifts, no SV writes).
  Eliminates Fabric/Reanimated race that caused 1-frame position jump.

- isDraggingSV race fix: removed early set in onActivate, added clear
  in onDeactivate/onFinalize. Prevents stale worklet runs between drags.

- Snap target cache: O(1) lookup at drag end instead of O(N) key walk.
  Boundaries use visual positions (base+shift) for permanent shifts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Split 958-line types.ts into 5 focused files: core, events, view,
  provider, sortable — plus barrel index.ts for backward-compatible imports
- Remove ~270 lines of dead/duplicate type definitions that were never
  imported (authoritative versions live in hooks/useSortableList.ts,
  hooks/useSortableBoard.ts, SortableBoardContext.ts)
- Expand hooks/index.ts barrel from 2 to 12 exports
- Export AnimatedViewStylePropWithoutLayout from public API
- Update CLAUDE.md with Code Organization section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route cell positions through SharedValues instead of React props.
Cells use translateX/Y (stacked transforms) for zero Yoga relayout.
Per-cell subscription via CellBindingStore + useSyncExternalStore
ensures only recycled cells re-render.

- RecycledCell: left:0/top:0 + stacked translateX/Y from basePositionSV + shiftSV
- CellBindingStore: per-cell subscription, positions excluded from binding data
- _contentPosition on DraxView: bypasses stale view.measure() for list cells
- Hover render: direct JS→JS forceRender (no SV→UI→JS bounce)
- cumulativeEndsSV: O(log N) binary search for single-column slot detection
- Binding refresh after commitReorder: fixes stale dataIndex after reorder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 28, 2026

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

Project Deployment Actions Updated (UTC)
docs-site Ready Ready Preview, Comment Mar 28, 2026 8:14pm
drax-example Ready Ready Preview, Comment Mar 28, 2026 8:14pm

Keep SV-based positioning (stacked transforms, per-cell subscription,
_contentPosition, cumulativeEndsSV binary search) from our branch.
Adopt upstream's inactiveItemStyle early-return pattern in RecycledCell.
Remove duplicate drag state refs from merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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