LearnNewsExamplesServices
Frontmatter
id9402
titlePerformance: Eliminate Main Thread Layout Thrashing in getTargetData
stateClosed
labels
enhancementaiperformance
assigneestobiu
createdAtMar 8, 2026, 11:27 PM
updatedAtMar 8, 2026, 11:46 PM
githubUrlhttps://github.com/neomjs/neo/issues/9402
authortobiu
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMar 8, 2026, 11:46 PM

Performance: Eliminate Main Thread Layout Thrashing in getTargetData

Closed v12.1.0 enhancementaiperformance
tobiu
tobiu commented on Mar 8, 2026, 11:27 PM

Problem: Profiling the Main Thread during rapid Grid scrolling reveals a massive layout thrashing penalty:

  • Recalculate style: 38.5% total time
  • get scrollTop & getBoundingClientRect: ~9% total time

This occurs because the generic src/main/DomEvents.mjs listener intercepts highly frequent scroll and wheel events and calls getTargetData(target) to build the event payload. getTargetData unconditionally reads node.getBoundingClientRect(), node.offsetHeight, node.scrollHeight, etc., for every node in the event path. When these reads occur immediately after the Main Thread has applied VDOM deltas (e.g., updating transforms for row recycling), the browser is forced to synchronously flush the layout queue and recalculate all styles just to return the unused rect data.

Proposed Solution: Pass the event.type into getTargetData(). If the event is a high-frequency continuous event that does not rely on bounding rects (specifically scroll and wheel), skip the getBoundingClientRect() and offset/client/scrollHeight reads entirely.

This prevents forced synchronous layouts and reclaims nearly 40% of the Main Thread's blocking overhead during scrolling.

DX Tradeoff Note: We must heavily document this change. Developers relying on data.path[n].rect inside onScroll handlers will now receive an empty object or null. This is an intentional architectural safeguard. If rects are truly needed during scrolling, they must be requested explicitly via Neo.main.DomAccess.getBoundingClientRect().

tobiu added the enhancement label on Mar 8, 2026, 11:27 PM
tobiu added the ai label on Mar 8, 2026, 11:27 PM
tobiu added the performance label on Mar 8, 2026, 11:27 PM
tobiu referenced in commit 067c823 - "perf: Eliminate Main Thread Layout Thrashing in getTargetData (#9402)" on Mar 8, 2026, 11:36 PM
tobiu assigned to @tobiu on Mar 8, 2026, 11:36 PM
tobiu
tobiu Mar 8, 2026, 11:46 PM

Input from Gemini 3.1 Pro:

Task Completed:

  • Added eventName to getTargetData and getSelectionPath in src/main/DomEvents.mjs.
  • Implemented a fast path: If eventName === 'scroll' || eventName === 'wheel', the framework no longer calls node.getBoundingClientRect() or reads offsetHeight/scrollHeight in getTargetData.
  • Retained these properties for all other events (clicks, dragging, selection) and addons (IntersectionObserver, ResizeObserver) by passing undefined by default.

Benchmark Review Note: Subsequent performance tracing revealed that while this successfully eliminated getBoundingClientRect overhead, the browser's forced synchronous layout simply shifted to the next geometric read: node.scrollTop (consuming 13.1% of Main Thread time).

This confirms the necessity of this initial pruning, but also proves we need a follow-up ticket to aggressively prune scrollTop/scrollLeft reads for ancestor path elements.

Closing this ticket as the specific getBoundingClientRect and dimension stripping has been implemented.

tobiu closed this issue on Mar 8, 2026, 11:46 PM