Fix piece-move animation stutter during analysis (fixes #631)#725
Fix piece-move animation stutter during analysis (fixes #631)#725DarrellThomas wants to merge 3 commits intofranciscoBSalgueiro:masterfrom
Conversation
…gueiro#631) Restore v0.11.1 architecture: parent subscribes to store once, passes isCurrentVariation/isStart/transpositions as props to CompleteMoveCell instead of each cell subscribing independently. Reduces selector evaluations per engine event from O(N) to O(1). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Line-length formatting for ternary expressions and imports to satisfy biome ci checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Worth noting: this PR and #530 / PR #532 are solving different sides of the same problem. Think of it like drinking from a firehose. This PR (#725) teaches you to swallow faster — it reduces the cost of each engine event hitting the UI. Before, every event triggered O(N) selector evaluations across all notation cells. Now the parent subscribes once and passes cheap props down. Each gulp is cheap instead of choking. PR #532 (@TurtleOrangina's fix) turns down the water pressure — it reduces the frequency of events by buffering on the Rust side. Instead of blasting the UI with every depth update (especially the garbage depths 1–5 in the first 200ms), it holds back and only sends updates the human can actually read. Either fix alone helps significantly. Together they're belt-and-suspenders — the UI receives fewer events AND each event costs less to process. The two PRs touch completely different files (Rust backend vs React frontend) so they merge cleanly with no conflicts. I've implemented both in my fork and the result is noticeably smoother than either fix alone. |
Keep props-based architecture (isCurrentVariation, isStart, transpositions passed from parent) — the core O(N)->O(1) fix. Pick up upstream's getTabFile refactor, remove unused useStoreWithEqualityFn import, reformat with oxfmt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Fixes #631 — piece-move animations stutter during multi-line engine analysis, a regression introduced between v0.11.1 and v0.12.0.
Root Cause
This is not a Tauri v2 issue. The stutter is caused by an O(N) rendering regression in the notation panel introduced across several commits between v0.11.1 and v0.12.0:
f07a1348("more efficient gamenotation memoization") — movedisCurrentVariationfrom a parent-computed prop to a per-cell store subscriptiona37a6db4("fix async jotai atoms") — movedisStartfrom a parent-computed prop to a per-cell store subscriptione4ed430d/8f85fef3("add icon to transpositions" / "cache transpositions") — added a per-cellgetTranspositions()store subscription that walks the entire game treeThe problem
In v0.11.1,
RenderVariationTreesubscribed tos.positiononce and passedisCurrentVariationandisStartas boolean props to eachCompleteMoveCell. Thememo()comparator could skip re-renders using cheap boolean/reference equality.In v0.12.0+, each
CompleteMoveCellindependently subscribes to the Zustand store:During engine analysis, every
bestMovesPayloadevent (up to 5/sec, rate-limited on the Rust side) triggerssetScore()on the Zustand store. This causes everyCompleteMoveCellto re-evaluate all three selectors.For a typical annotated game with N notation cells (80 half-moves + variations ≈ 120 cells), each engine event triggers:
equal(s.position, movePath)evaluationsequal(movePath, s.headers.start)evaluationsgetTranspositions()tree walksAt 5 engine events/sec with MultiPV 3, that's ~1,800 selector evaluations per second on the main thread, each involving
fast-deep-equalarray comparisons and (for transpositions) full game tree iteration. This directly competes with CSS animation frames, causing visible stutter on the chessground piece animations.The
memo()comparator onCompleteMoveCellalso lost itsisCurrentVariationandisStartchecks when those became internal store reads, meaning it can no longer prevent re-renders for those value changes.The Fix
Restores the v0.11.1 architecture: parent subscribes once, passes cheap props down.
CompleteMoveCell(3 store subscriptions removed):isCurrentVariation,isStart, andtranspositionsbecome props instead of internal store subscriptionsmemo()comparator updated to include these props againsrc/utils/transpositions.ts(unchanged, just moved)GameNotation.tsx(RenderVariationTreeandRowSegment):s.position,s.headers.start, ands.rootonce per tree levelisCurrentVariation,isStart, and transpositions for each childCompleteMoveCellThe transposition feature (icons, click-to-navigate) is fully preserved — only where the computation happens has changed.
Result
CompleteMoveCellRenderVariationTreelevelmemo()can skip re-renders for position changesVerification
We built and tested three versions side-by-side using separate git worktrees:
All three were run against the same game (Morozevich vs. Bologan, Italian Game) with Komodo engine analysis at 4 lines / 4 cores. We recorded screen captures and extracted frame sequences at 10fps for comparison.
Observations:
Transparency note: Testing was performed on a high-end workstation (AMD Threadripper, 96 cores). The stutter was subtle but measurable in frame data. On more typical hardware like the i9-13900 reported in #631, the effect would be significantly more pronounced since the O(N) selector work would consume a larger fraction of each 16ms frame budget. We would appreciate confirmation from the original reporter.
Files Changed
src/components/common/CompleteMoveCell.tsx— Remove 3 per-instance store subscriptions, accept as props instead. Restorememo()comparator.src/components/common/GameNotation.tsx—RenderVariationTreeandRowSegmentsubscribe once and pass computed props down.src/utils/transpositions.ts— Extracted from CompleteMoveCell (code unchanged, just relocated).