Frontmatter
| id | 9402 |
| title | Performance: Eliminate Main Thread Layout Thrashing in getTargetData |
| state | Closed |
| labels | enhancementaiperformance |
| assignees | tobiu |
| createdAt | Mar 8, 2026, 11:27 PM |
| updatedAt | Mar 8, 2026, 11:46 PM |
| githubUrl | https://github.com/neomjs/neo/issues/9402 |
| author | tobiu |
| commentsCount | 1 |
| parentIssue | null |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
| closedAt | Mar 8, 2026, 11:46 PM |
Performance: Eliminate Main Thread Layout Thrashing in getTargetData
tobiu assigned to @tobiu on Mar 8, 2026, 11:36 PM

tobiu
Mar 8, 2026, 11:46 PM
Input from Gemini 3.1 Pro:
✦ Task Completed:
- Added
eventNametogetTargetDataandgetSelectionPathinsrc/main/DomEvents.mjs.- Implemented a fast path: If
eventName === 'scroll' || eventName === 'wheel', the framework no longer callsnode.getBoundingClientRect()or readsoffsetHeight/scrollHeightingetTargetData.- Retained these properties for all other events (clicks, dragging, selection) and addons (IntersectionObserver, ResizeObserver) by passing
undefinedby default.Benchmark Review Note: Subsequent performance tracing revealed that while this successfully eliminated
getBoundingClientRectoverhead, 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/scrollLeftreads for ancestor path elements.Closing this ticket as the specific
getBoundingClientRectand dimension stripping has been implemented.
tobiu closed this issue on Mar 8, 2026, 11:46 PM
Problem: Profiling the Main Thread during rapid Grid scrolling reveals a massive layout thrashing penalty:
Recalculate style: 38.5% total timeget scrollTop&getBoundingClientRect: ~9% total timeThis occurs because the generic
src/main/DomEvents.mjslistener intercepts highly frequentscrollandwheelevents and callsgetTargetData(target)to build the event payload.getTargetDataunconditionally readsnode.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 unusedrectdata.Proposed Solution: Pass the
event.typeintogetTargetData(). If the event is a high-frequency continuous event that does not rely on bounding rects (specificallyscrollandwheel), skip thegetBoundingClientRect()andoffset/client/scrollHeightreads 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].rectinsideonScrollhandlers 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 viaNeo.main.DomAccess.getBoundingClientRect().