Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 76 additions & 13 deletions Abies.Presentation/wwwroot/abies.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,36 @@ const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotne

const registeredEvents = new Set();

// Pre-register all common event types at startup to avoid O(n) DOM scanning
// on every incremental update. These match the event types defined in Operations.cs.
const COMMON_EVENT_TYPES = [
// Mouse events
'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
'mouseenter', 'mouseleave', 'mousemove', 'contextmenu', 'wheel',
// Keyboard events
'keydown', 'keyup', 'keypress',
// Form events
'input', 'change', 'submit', 'reset', 'focus', 'blur', 'invalid', 'search',
// Touch events
'touchstart', 'touchend', 'touchmove', 'touchcancel',
// Pointer events
'pointerdown', 'pointerup', 'pointermove', 'pointercancel',
'pointerover', 'pointerout', 'pointerenter', 'pointerleave',
'gotpointercapture', 'lostpointercapture',
// Drag events
'drag', 'dragstart', 'dragend', 'dragenter', 'dragleave', 'dragover', 'drop',
// Clipboard events
'copy', 'cut', 'paste',
// Media events
'play', 'pause', 'ended', 'volumechange', 'timeupdate', 'seeking', 'seeked',
'loadeddata', 'loadedmetadata', 'canplay', 'canplaythrough', 'playing',
'waiting', 'stalled', 'suspend', 'emptied', 'ratechange', 'durationchange',
// Other events
'scroll', 'resize', 'load', 'error', 'abort', 'select', 'toggle',
'animationstart', 'animationend', 'animationiteration', 'animationcancel',
'transitionstart', 'transitionend', 'transitionrun', 'transitioncancel'
];

function ensureEventListener(eventName) {
if (registeredEvents.has(eventName)) return;
// Attach to document to survive body innerHTML changes and use capture for early handling
Expand All @@ -459,6 +489,10 @@ function ensureEventListener(eventName) {
registeredEvents.add(eventName);
}

// Pre-register all common event types once at module load
// This eliminates the need for O(n) DOM scanning on every update
COMMON_EVENT_TYPES.forEach(ensureEventListener);

// Helper to find an element with a specific attribute, traversing through shadow DOM boundaries
function findEventTarget(event, attributeName) {
// First try the composed path to handle shadow DOM (for Web Components like fluent-button)
Expand Down Expand Up @@ -811,24 +845,53 @@ function unsubscribe(key) {
}

/**
* Adds event listeners to the document body for interactive elements.
* Discovers and registers event listeners for any custom (non-common) event types
* in the given DOM subtree. Since common event types are pre-registered at startup,
* this function only needs to scan for rare/custom event handlers.
*
* For performance, this is now a no-op for most apps since all standard DOM events
* are pre-registered. Custom event names will still be discovered via attribute
* updates (UpdateAttribute/AddAttribute patches call ensureEventListener directly).
*/
function addEventListeners(root) {
// All common event types are pre-registered at startup.
// This function now only handles edge cases where:
// 1. Initial page load contains custom (non-standard) event types
// 2. HTML injection bypasses attribute patching
//
// For the vast majority of cases, this is a no-op since:
// - Common events are pre-registered
// - Attribute patches call ensureEventListener directly
//
// We keep the function signature for backward compatibility but make it
// much more efficient by only scanning when necessary.

// Skip scanning entirely during incremental updates since:
// 1. Common events are already registered
// 2. Attribute patches handle event listener registration
if (root && root !== document) {
// For incremental updates (AddChild, ReplaceChild), skip expensive DOM scanning
// The pre-registered common events cover 99%+ of use cases
return;
}

// Full page render (setAppContent) - scan once for any custom event types
// This only happens on initial load or full page navigation
const scope = root || document;
// Build a list including the scope element (if Element) plus all descendants
const nodes = [];
if (scope && scope.nodeType === 1 /* ELEMENT_NODE */) nodes.push(scope);
scope.querySelectorAll('*').forEach(el => {
nodes.push(el);
});
nodes.forEach(el => {
for (const attr of el.attributes) {
if (attr.name.startsWith('data-event-')) {
const name = attr.name.substring('data-event-'.length);
ensureEventListener(name);
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);
let el = walker.currentNode;
while (el) {
if (el.attributes) {
for (const attr of el.attributes) {
if (attr.name.startsWith('data-event-')) {
const name = attr.name.substring('data-event-'.length);
// Only registers if not already in registeredEvents (common events are already there)
ensureEventListener(name);
}
}
}
});
el = walker.nextNode();
}
}

/**
Expand Down
89 changes: 76 additions & 13 deletions Abies.SubscriptionsDemo/wwwroot/abies.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,36 @@ const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotne

const registeredEvents = new Set();

// Pre-register all common event types at startup to avoid O(n) DOM scanning
// on every incremental update. These match the event types defined in Operations.cs.
const COMMON_EVENT_TYPES = [
// Mouse events
'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
'mouseenter', 'mouseleave', 'mousemove', 'contextmenu', 'wheel',
// Keyboard events
'keydown', 'keyup', 'keypress',
// Form events
'input', 'change', 'submit', 'reset', 'focus', 'blur', 'invalid', 'search',
// Touch events
'touchstart', 'touchend', 'touchmove', 'touchcancel',
// Pointer events
'pointerdown', 'pointerup', 'pointermove', 'pointercancel',
'pointerover', 'pointerout', 'pointerenter', 'pointerleave',
'gotpointercapture', 'lostpointercapture',
// Drag events
'drag', 'dragstart', 'dragend', 'dragenter', 'dragleave', 'dragover', 'drop',
// Clipboard events
'copy', 'cut', 'paste',
// Media events
'play', 'pause', 'ended', 'volumechange', 'timeupdate', 'seeking', 'seeked',
'loadeddata', 'loadedmetadata', 'canplay', 'canplaythrough', 'playing',
'waiting', 'stalled', 'suspend', 'emptied', 'ratechange', 'durationchange',
// Other events
'scroll', 'resize', 'load', 'error', 'abort', 'select', 'toggle',
'animationstart', 'animationend', 'animationiteration', 'animationcancel',
'transitionstart', 'transitionend', 'transitionrun', 'transitioncancel'
];

function ensureEventListener(eventName) {
if (registeredEvents.has(eventName)) return;
// Attach to document to survive body innerHTML changes and use capture for early handling
Expand All @@ -459,6 +489,10 @@ function ensureEventListener(eventName) {
registeredEvents.add(eventName);
}

// Pre-register all common event types once at module load
// This eliminates the need for O(n) DOM scanning on every update
COMMON_EVENT_TYPES.forEach(ensureEventListener);

// Helper to find an element with a specific attribute, traversing through shadow DOM boundaries
function findEventTarget(event, attributeName) {
// First try the composed path to handle shadow DOM (for Web Components like fluent-button)
Expand Down Expand Up @@ -811,24 +845,53 @@ function unsubscribe(key) {
}

/**
* Adds event listeners to the document body for interactive elements.
* Discovers and registers event listeners for any custom (non-common) event types
* in the given DOM subtree. Since common event types are pre-registered at startup,
* this function only needs to scan for rare/custom event handlers.
*
* For performance, this is now a no-op for most apps since all standard DOM events
* are pre-registered. Custom event names will still be discovered via attribute
* updates (UpdateAttribute/AddAttribute patches call ensureEventListener directly).
*/
function addEventListeners(root) {
// All common event types are pre-registered at startup.
// This function now only handles edge cases where:
// 1. Initial page load contains custom (non-standard) event types
// 2. HTML injection bypasses attribute patching
//
// For the vast majority of cases, this is a no-op since:
// - Common events are pre-registered
// - Attribute patches call ensureEventListener directly
//
// We keep the function signature for backward compatibility but make it
// much more efficient by only scanning when necessary.

// Skip scanning entirely during incremental updates since:
// 1. Common events are already registered
// 2. Attribute patches handle event listener registration
if (root && root !== document) {
// For incremental updates (AddChild, ReplaceChild), skip expensive DOM scanning
// The pre-registered common events cover 99%+ of use cases
return;
}

// Full page render (setAppContent) - scan once for any custom event types
// This only happens on initial load or full page navigation
const scope = root || document;
// Build a list including the scope element (if Element) plus all descendants
const nodes = [];
if (scope && scope.nodeType === 1 /* ELEMENT_NODE */) nodes.push(scope);
scope.querySelectorAll('*').forEach(el => {
nodes.push(el);
});
nodes.forEach(el => {
for (const attr of el.attributes) {
if (attr.name.startsWith('data-event-')) {
const name = attr.name.substring('data-event-'.length);
ensureEventListener(name);
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);
let el = walker.currentNode;
while (el) {
if (el.attributes) {
for (const attr of el.attributes) {
if (attr.name.startsWith('data-event-')) {
const name = attr.name.substring('data-event-'.length);
// Only registers if not already in registeredEvents (common events are already there)
ensureEventListener(name);
}
}
}
});
el = walker.nextNode();
}
}

/**
Expand Down
89 changes: 76 additions & 13 deletions Abies/wwwroot/abies.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,36 @@ const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotne

const registeredEvents = new Set();

// Pre-register all common event types at startup to avoid O(n) DOM scanning
// on every incremental update. These match the event types defined in Operations.cs.
const COMMON_EVENT_TYPES = [
// Mouse events
'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
'mouseenter', 'mouseleave', 'mousemove', 'contextmenu', 'wheel',
// Keyboard events
'keydown', 'keyup', 'keypress',
// Form events
'input', 'change', 'submit', 'reset', 'focus', 'blur', 'invalid', 'search',
// Touch events
'touchstart', 'touchend', 'touchmove', 'touchcancel',
// Pointer events
'pointerdown', 'pointerup', 'pointermove', 'pointercancel',
'pointerover', 'pointerout', 'pointerenter', 'pointerleave',
'gotpointercapture', 'lostpointercapture',
// Drag events
'drag', 'dragstart', 'dragend', 'dragenter', 'dragleave', 'dragover', 'drop',
// Clipboard events
'copy', 'cut', 'paste',
// Media events
'play', 'pause', 'ended', 'volumechange', 'timeupdate', 'seeking', 'seeked',
'loadeddata', 'loadedmetadata', 'canplay', 'canplaythrough', 'playing',
'waiting', 'stalled', 'suspend', 'emptied', 'ratechange', 'durationchange',
// Other events
'scroll', 'resize', 'load', 'error', 'abort', 'select', 'toggle',
'animationstart', 'animationend', 'animationiteration', 'animationcancel',
'transitionstart', 'transitionend', 'transitionrun', 'transitioncancel'
Comment on lines +454 to +481
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

COMMON_EVENT_TYPES pre-registers high-frequency events like mousemove, pointermove, scroll, and wheel. Because genericEventHandler always calls event.composedPath() and walks the path even when no data-event-* attribute exists, this adds steady-state overhead for every one of those events in every app (even if the app never uses them). Consider limiting pre-registration to a small set of low-frequency “always used” events (e.g., click/input/change/submit/keydown/keyup) and keep lazy registration (via attribute patches or subtree scan) for the rest.

Copilot uses AI. Check for mistakes.
];

function ensureEventListener(eventName) {
if (registeredEvents.has(eventName)) return;
// Attach to document to survive body innerHTML changes and use capture for early handling
Expand All @@ -459,6 +489,10 @@ function ensureEventListener(eventName) {
registeredEvents.add(eventName);
}

// Pre-register all common event types once at module load
// This eliminates the need for O(n) DOM scanning on every update
COMMON_EVENT_TYPES.forEach(ensureEventListener);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

COMMON_EVENT_TYPES.forEach(ensureEventListener) runs before exports is initialized (const exports = await getAssemblyExports(...) later in the module). If a user interacts with an element that already has a data-event-* attribute during startup, genericEventHandler will run and can throw a ReferenceError due to exports being in the TDZ. To avoid startup crashes, delay registering these listeners until after exports is assigned (and ideally after runMain()), or change exports to a let initialized to null up-front and guard in genericEventHandler when exports/runtime isn’t ready yet.

Suggested change
// Pre-register all common event types once at module load
// This eliminates the need for O(n) DOM scanning on every update
COMMON_EVENT_TYPES.forEach(ensureEventListener);
// Pre-register all common event types after initial document load instead of
// during module evaluation. This avoids invoking genericEventHandler while
// the runtime exports may still be in the temporal dead zone.
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', () => {
COMMON_EVENT_TYPES.forEach(ensureEventListener);
}, { once: true });
} else {
COMMON_EVENT_TYPES.forEach(ensureEventListener);
}

Copilot uses AI. Check for mistakes.

// Helper to find an element with a specific attribute, traversing through shadow DOM boundaries
function findEventTarget(event, attributeName) {
// First try the composed path to handle shadow DOM (for Web Components like fluent-button)
Expand Down Expand Up @@ -811,24 +845,53 @@ function unsubscribe(key) {
}

/**
* Adds event listeners to the document body for interactive elements.
* Discovers and registers event listeners for any custom (non-common) event types
* in the given DOM subtree. Since common event types are pre-registered at startup,
* this function only needs to scan for rare/custom event handlers.
*
* For performance, this is now a no-op for most apps since all standard DOM events
* are pre-registered. Custom event names will still be discovered via attribute
* updates (UpdateAttribute/AddAttribute patches call ensureEventListener directly).
*/
function addEventListeners(root) {
// All common event types are pre-registered at startup.
// This function now only handles edge cases where:
// 1. Initial page load contains custom (non-standard) event types
// 2. HTML injection bypasses attribute patching
//
// For the vast majority of cases, this is a no-op since:
// - Common events are pre-registered
// - Attribute patches call ensureEventListener directly
//
// We keep the function signature for backward compatibility but make it
// much more efficient by only scanning when necessary.

// Skip scanning entirely during incremental updates since:
// 1. Common events are already registered
// 2. Attribute patches handle event listener registration
if (root && root !== document) {
// For incremental updates (AddChild, ReplaceChild), skip expensive DOM scanning
// The pre-registered common events cover 99%+ of use cases
return;
}

// Full page render (setAppContent) - scan once for any custom event types
// This only happens on initial load or full page navigation
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addEventListeners(root) now returns early for any subtree root (root && root !== document). However, AddChild/ReplaceChild patches insert raw HTML fragments and then call addEventListeners(childElement/newNode) specifically to discover data-event-* attributes in the inserted subtree. With the early return, any non-pre-registered (custom/rare) event types in newly added HTML will never be registered, breaking event dispatch. Either keep scanning for subtree roots (TreeWalker already makes it cheaper), or ensure the AddChild/ReplaceChild patch path registers event types without relying on a DOM scan.

Suggested change
// Skip scanning entirely during incremental updates since:
// 1. Common events are already registered
// 2. Attribute patches handle event listener registration
if (root && root !== document) {
// For incremental updates (AddChild, ReplaceChild), skip expensive DOM scanning
// The pre-registered common events cover 99%+ of use cases
return;
}
// Full page render (setAppContent) - scan once for any custom event types
// This only happens on initial load or full page navigation
// Full page render (setAppContent) or subtree scan (AddChild/ReplaceChild) -
// scan for any custom event types in the given scope.

Copilot uses AI. Check for mistakes.
const scope = root || document;
// Build a list including the scope element (if Element) plus all descendants
const nodes = [];
if (scope && scope.nodeType === 1 /* ELEMENT_NODE */) nodes.push(scope);
scope.querySelectorAll('*').forEach(el => {
nodes.push(el);
});
nodes.forEach(el => {
for (const attr of el.attributes) {
if (attr.name.startsWith('data-event-')) {
const name = attr.name.substring('data-event-'.length);
ensureEventListener(name);
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);
let el = walker.currentNode;
while (el) {
if (el.attributes) {
for (const attr of el.attributes) {
if (attr.name.startsWith('data-event-')) {
const name = attr.name.substring('data-event-'.length);
// Only registers if not already in registeredEvents (common events are already there)
ensureEventListener(name);
}
}
}
});
el = walker.nextNode();
}
}

/**
Expand Down
Loading