Skip to content

Conversation

@simonklee
Copy link
Contributor

@simonklee simonklee commented Jan 8, 2026

So, the core issue was that scrolling or animating elements left the hover state stale because we were only running hit tests on actual mouse movement. If the element moved but the mouse didn't, we wouldn't catch it.

To fix this without tanking performance during scroll, we implemented a dirty-flag + debounce system:

  1. Dirty Tracking: I added a hitGridDirty flag to the renderer. Now, whenever a Renderable changes a property that affects hit testing (translateX/Y, zIndex, visible, overflow, or layout shifts), it calls markHitGridDirty().

  2. Debounced Recheck: At the end of the render loop, if the grid is dirty, we schedule a debounced hover recheck. This groups many updates into a single check instead of running hit logic every frame.

  3. Synthetic Events: The recheck uses the last known pointer position. If the target under the cursor changes, we fire out and over events to update the UI.

I cancel this check if the user moves the mouse, since that triggers an immediate test. It's also skipped during drags to avoid messing with drop targets.

@simonklee simonklee marked this pull request as ready for review January 9, 2026 10:07
- track hit-grid dirty on layout/translate/visibility/z-index changes
- schedule post-frame hover rechecks with debounce and move/drag cancel
…ates

Debounced hover rechecks to avoid stale pointer payloads and
re-entrancy, and made z-index flips trigger a frame so hit/hover
updates land reliably after render.
@kommander
Copy link
Collaborator

I didn't want the manual markHitgridDirty as it adds complexity and is error prone, while the native side can easily know that the hitgrid changed. Similar for tracking and managing the debounce. Here is what I measured as impact for running it based off the native hitgridDirty flag:

Operation Avg P99
getHitGridDirty (FFI call) 0.109μs 1.25μs
hitTest (FFI call) 0.155μs 1.63μs
recheckHoverState - no change 0.201μs 0.88μs
recheckHoverState - with change (fires events) 0.458μs 2.08μs

Impact on frame budget:

  • At 60fps, frame budget is 16,667μs
  • Worst case (with hover change): ~0.5μs = 0.003% of frame budget

Could be slightly improved by exposing an FFI method to checkHitgridChangedAt, pass the cursor position and just compare that cell in prev/current hitgrid. Or use hitgrid pointers and do the comparison in JS, not sure which is cheaper here, probably won't make much of a difference.

@simonklee
Copy link
Contributor Author

I didn't want the manual markHitgridDirty as it adds complexity and is error prone, while the native side can easily know that the hitgrid changed. Similar for tracking and managing the debounce. Here is what I measured as impact for running it based off the native hitgridDirty flag:

Operation Avg P99
getHitGridDirty (FFI call) 0.109μs 1.25μs
hitTest (FFI call) 0.155μs 1.63μs
recheckHoverState - no change 0.201μs 0.88μs
recheckHoverState - with change (fires events) 0.458μs 2.08μs
Impact on frame budget:

  • At 60fps, frame budget is 16,667μs
  • Worst case (with hover change): ~0.5μs = 0.003% of frame budget

Could be slightly improved by exposing an FFI method to checkHitgridChangedAt, pass the cursor position and just compare that cell in prev/current hitgrid. Or use hitgrid pointers and do the comparison in JS, not sure which is cheaper here, probably won't make much of a difference.

I think this is a fair trade-off, with debounce removed, hover rechecks now run immediately after every render when the hit grid changes, but we don't have to track this + extra complexity.

@kommander kommander merged commit bc429f7 into anomalyco:main Jan 12, 2026
4 checks passed
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.

2 participants