Repository navigation
perf(ui): drive Kandy wander position imperatively to stop idle re-render loop - #22
Conversation
|
| Filename | Overview |
|---|---|
| ui/bundle.js | Introduces imperative wander-node positioning and boundary-only state publication, but state-positioned speech and gaze remain stale during an active leg. |
| ui/bundle.test.js | Covers callback-ref wiring and lifecycle basics but does not exercise the interval, active-leg restoration, render suppression, or boundary synchronization. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
T[Wander interval] --> M[Update live motionRef.x]
M -->|Mid-leg| D[Write transform directly to wander node]
M -->|Leg boundary| S[Publish React motion state]
D --> W[Mascot wrapper moves]
S --> R[React render updates structural state]
R --> O[Sibling overlays receive motionState.x]
D -. live x not published .-> O
Reviews (1): Last reviewed commit: "perf(ui): drive Kandy wander position im..." | Re-trigger Greptile
| // instead of re-rendering the whole mascot tree. | ||
| applyWanderX(m.x); | ||
| }, WANDER_FRAME_MS); |
There was a problem hiding this comment.
Mid-leg updates move only the wander wrapper, leaving motionState.x at the previous boundary. Speech can appear without stopping the walk, but its sibling overlay is positioned from that stale state, so its anchor can be up to 35px away from the visible mascot. Pointer-driven gaze calculations also use the stale boundary position during a leg.
Knowledge Base Used: Client interface
Problem
On an idle Kandev tab the app climbed to ~2.1 GB RSS and ~40% CPU. Profiling
traced it to the Kandy widget, not the host app:
from ~62% to ~13% and roughly doubled idle time.
KandyWidgetupdating on its ownState (#16)(the motion state), not any host selector.The mechanism: while a wander leg plays,
beginWanderFramesruns asetIntervalatWANDER_FRAME_MS(40ms, ~25fps) that callspublishMotion()every tick. Each call does
setMotionState, re-rendering the entire mascot SVGtree ~25 times per second. In React 19 dev mode every render also emits
performance.measure()calls, which accumulate unbounded in Chrome's nativeUser Timing buffer, which is where the multi-GB heap came from.
Fix
Drive the per-frame leg position imperatively and keep React state only for
structural changes:
motion.wanderRef) registers the.kandev-kandy-wandernode(s). Mid-leg, the interval writes
transform: translateX(x px)straight tothose nodes via
applyWanderX(x), with no React render.publishMotion()(and thereforesetMotionState) now fires only at legboundaries (leg start/end), on facing flips, and on cry state, exactly as
before, just not on every frame.
useLayoutEffectreasserts the live position after any unrelated commit(data refresh, clock tick) so the node never flashes back to the last
boundary value mid-stroll.
mid-leg does not show a stale position.
Visual behavior is unchanged: same 25fps cadence, same easing, same facing and
gait wiring. The only difference is that mid-leg frames no longer re-render the
component tree, which removes both the idle CPU loop and the dev-mode measure
flood that fed the memory growth.
Tests
node --test ui/bundle.test.js(94 pass). Added:motion.wanderRefis forwarded onto the wander layer (andis absent when the caller omits it);
node position on attach, and returns an unregister cleanup.
Summary by cubic
Drives the Kandy wander leg position imperatively on the wander layer, eliminating the per-frame React re-render that caused an idle tab to climb to ~2.1 GB RSS and ~40% CPU. Also declares
plugin-packas a tool dependency so the package build resolvesjsonschema.translateXstraight to it.useLayoutEffectreasserts the live position after unrelated commits to avoid flashes.Written for commit a13584a. Summary will update on new commits.