diff --git a/src/myfasthtml/assets/myfasthtml.js b/src/myfasthtml/assets/myfasthtml.js
index f0a8c0a..16e159b 100644
--- a/src/myfasthtml/assets/myfasthtml.js
+++ b/src/myfasthtml/assets/myfasthtml.js
@@ -185,58 +185,72 @@ function bindTooltipsWithDelegation(elementId) {
return;
}
+ // OPTIMIZATION C: Throttling flag to limit mouseenter processing
+ let tooltipRafScheduled = false;
+
// Add a single mouseenter and mouseleave listener to the parent element
element.addEventListener("mouseenter", (event) => {
- //console.debug("Entering element", event.target)
+ // OPTIMIZATION C: Early exit - check mf-no-tooltip FIRST (before any DOM work)
+ if (element.hasAttribute("mf-no-tooltip")) {
+ return;
+ }
+
+ // OPTIMIZATION C: Throttle mouseenter events (max 1 per frame)
+ if (tooltipRafScheduled) {
+ return;
+ }
const cell = event.target.closest("[data-tooltip]");
if (!cell) {
- // console.debug(" No 'data-tooltip' attribute found. Stopping.");
return;
}
- const no_tooltip = element.hasAttribute("mf-no-tooltip");
- if (no_tooltip) {
- // console.debug(" Attribute 'mmt-no-tooltip' found. Cancelling.");
- return;
- }
+ // OPTIMIZATION C: Move ALL DOM reads into RAF to avoid forced synchronous layouts
+ tooltipRafScheduled = true;
+ requestAnimationFrame(() => {
+ tooltipRafScheduled = false;
- const content = cell.querySelector(".truncate") || cell;
- const isOverflowing = content.scrollWidth > content.clientWidth;
- const forceShow = cell.classList.contains("mf-tooltip");
+ // Check again in case tooltip was disabled during RAF delay
+ if (element.hasAttribute("mf-no-tooltip")) {
+ return;
+ }
- if (isOverflowing || forceShow) {
- const tooltipText = cell.getAttribute("data-tooltip");
- if (tooltipText) {
- const rect = cell.getBoundingClientRect();
- const tooltipRect = tooltipContainer.getBoundingClientRect();
+ // All DOM reads happen here (batched in RAF)
+ const content = cell.querySelector(".truncate") || cell;
+ const isOverflowing = content.scrollWidth > content.clientWidth;
+ const forceShow = cell.classList.contains("mf-tooltip");
- let top = rect.top - 30; // Above the cell
- let left = rect.left;
+ if (isOverflowing || forceShow) {
+ const tooltipText = cell.getAttribute("data-tooltip");
+ if (tooltipText) {
+ const rect = cell.getBoundingClientRect();
+ const tooltipRect = tooltipContainer.getBoundingClientRect();
- // Adjust tooltip position to prevent it from going off-screen
- if (top < 0) top = rect.bottom + 5; // Move below if no space above
- if (left + tooltipRect.width > window.innerWidth) {
- left = window.innerWidth - tooltipRect.width - 5; // Prevent overflow right
- }
+ let top = rect.top - 30; // Above the cell
+ let left = rect.left;
- // Apply styles for tooltip positioning
- requestAnimationFrame(() => {
+ // Adjust tooltip position to prevent it from going off-screen
+ if (top < 0) top = rect.bottom + 5; // Move below if no space above
+ if (left + tooltipRect.width > window.innerWidth) {
+ left = window.innerWidth - tooltipRect.width - 5; // Prevent overflow right
+ }
+
+ // Apply styles (already in RAF)
tooltipContainer.textContent = tooltipText;
tooltipContainer.setAttribute("data-visible", "true");
tooltipContainer.style.top = `${top}px`;
tooltipContainer.style.left = `${left}px`;
- });
+ }
}
- }
- }, true); // Use capture phase for better delegation if needed
+ });
+ }); // OPTIMIZATION C: Removed capture phase (not needed)
element.addEventListener("mouseleave", (event) => {
const cell = event.target.closest("[data-tooltip]");
if (cell) {
tooltipContainer.setAttribute("data-visible", "false");
}
- }, true); // Use capture phase for better delegation if needed
+ }); // OPTIMIZATION C: Removed capture phase (not needed)
}
function initLayout(elementId) {
@@ -1132,7 +1146,11 @@ function updateTabs(controllerId) {
* @param {MouseEvent} event - The mouse event
*/
function handleGlobalClick(event) {
- console.debug("Global click detected");
+ // DEBUG: Measure click handler performance
+ const clickStart = performance.now();
+ const elementCount = MouseRegistry.elements.size;
+
+ console.warn(`🖱️ Click handler START: processing ${elementCount} registered elements`);
// Create a snapshot of current mouse action with modifiers
const snapshot = createSnapshot(event, 'click');
@@ -1151,8 +1169,10 @@ function updateTabs(controllerId) {
const currentMatches = [];
let anyHasLongerSequence = false;
let foundAnyMatch = false;
+ let iterationCount = 0;
for (const [elementId, data] of MouseRegistry.elements) {
+ iterationCount++;
const element = document.getElementById(elementId);
if (!element) continue;
@@ -1246,6 +1266,13 @@ function updateTabs(controllerId) {
if (MouseRegistry.snapshotHistory.length > 10) {
MouseRegistry.snapshotHistory = [];
}
+
+ // DEBUG: Log click handler performance
+ const clickDuration = performance.now() - clickStart;
+ console.warn(`🖱️ Click handler DONE: ${clickDuration.toFixed(2)}ms (${iterationCount} iterations, ${currentMatches.length} matches)`);
+ if (clickDuration > 100) {
+ console.warn(`⚠️ SLOW CLICK HANDLER: ${clickDuration.toFixed(2)}ms for ${elementCount} elements`);
+ }
}
/**
@@ -1478,8 +1505,6 @@ function updateTabs(controllerId) {
* @param {string} gridId - The ID of the DataGrid instance
*/
function initDataGridScrollbars(gridId) {
- console.debug("initDataGridScrollbars on element " + gridId);
-
const wrapper = document.getElementById(`tw_${gridId}`);
if (!wrapper) {
@@ -1506,188 +1531,167 @@ function initDataGridScrollbars(gridId) {
// OPTIMIZATION: RequestAnimationFrame flags to throttle visual updates
let rafScheduledVertical = false;
let rafScheduledHorizontal = false;
+ let rafScheduledUpdate = false;
- const computeScrollbarVisibility = () => {
- // Vertical: check if body content exceeds body container height
- const isVerticalRequired = bodyContainer.scrollHeight > bodyContainer.clientHeight;
+ // OPTIMIZATION: Pre-calculated scroll ratios (updated in updateScrollbars)
+ // Allows instant mousedown with zero DOM reads
+ let cachedVerticalScrollRatio = 0;
+ let cachedHorizontalScrollRatio = 0;
- // Horizontal: check if content width exceeds table width (use cached references)
- const contentWidth = Math.max(
- header ? header.scrollWidth : 0,
- body ? body.scrollWidth : 0
- );
- const isHorizontalRequired = contentWidth > table.clientWidth;
+ // OPTIMIZATION: Cached scroll positions to avoid DOM reads in mousedown
+ // Initialized once at setup, updated in RAF handlers after each scroll change
+ let cachedBodyScrollTop = bodyContainer.scrollTop;
+ let cachedTableScrollLeft = table.scrollLeft;
+
+ /**
+ * OPTIMIZED: Batched update function
+ * Phase 1: Read all DOM properties (no writes)
+ * Phase 2: Calculate all values
+ * Phase 3: Write all DOM properties in single RAF
+ */
+ const updateScrollbars = () => {
+ if (rafScheduledUpdate) return;
+
+ rafScheduledUpdate = true;
requestAnimationFrame(() => {
+ rafScheduledUpdate = false;
+
+ // PHASE 1: Read all DOM properties
+ const metrics = {
+ bodyScrollHeight: bodyContainer.scrollHeight,
+ bodyClientHeight: bodyContainer.clientHeight,
+ bodyScrollTop: bodyContainer.scrollTop,
+ tableClientWidth: table.clientWidth,
+ tableScrollLeft: table.scrollLeft,
+ verticalWrapperHeight: verticalWrapper.offsetHeight,
+ horizontalWrapperWidth: horizontalWrapper.offsetWidth,
+ headerScrollWidth: header ? header.scrollWidth : 0,
+ bodyScrollWidth: body ? body.scrollWidth : 0
+ };
+
+ // PHASE 2: Calculate all values
+
+ const contentWidth = Math.max(metrics.headerScrollWidth, metrics.bodyScrollWidth);
+
+ // Visibility
+ const isVerticalRequired = metrics.bodyScrollHeight > metrics.bodyClientHeight;
+ const isHorizontalRequired = contentWidth > metrics.tableClientWidth;
+
+ // Scrollbar sizes
+ let scrollbarHeight = 0;
+ if (metrics.bodyScrollHeight > 0) {
+ scrollbarHeight = (metrics.bodyClientHeight / metrics.bodyScrollHeight) * metrics.verticalWrapperHeight;
+ }
+
+ let scrollbarWidth = 0;
+ if (contentWidth > 0) {
+ scrollbarWidth = (metrics.tableClientWidth / contentWidth) * metrics.horizontalWrapperWidth;
+ }
+
+ // Scrollbar positions
+ const maxScrollTop = metrics.bodyScrollHeight - metrics.bodyClientHeight;
+ let verticalTop = 0;
+ if (maxScrollTop > 0) {
+ const scrollRatio = metrics.verticalWrapperHeight / metrics.bodyScrollHeight;
+ verticalTop = metrics.bodyScrollTop * scrollRatio;
+ }
+
+ const maxScrollLeft = contentWidth - metrics.tableClientWidth;
+ let horizontalLeft = 0;
+ if (maxScrollLeft > 0 && contentWidth > 0) {
+ const scrollRatio = metrics.horizontalWrapperWidth / contentWidth;
+ horizontalLeft = metrics.tableScrollLeft * scrollRatio;
+ }
+
+ // OPTIMIZATION: Pre-calculate and cache scroll ratios for instant mousedown
+ // Vertical scroll ratio
+ if (maxScrollTop > 0 && scrollbarHeight > 0) {
+ cachedVerticalScrollRatio = maxScrollTop / (metrics.verticalWrapperHeight - scrollbarHeight);
+ } else {
+ cachedVerticalScrollRatio = 0;
+ }
+
+ // Horizontal scroll ratio
+ if (maxScrollLeft > 0 && scrollbarWidth > 0) {
+ cachedHorizontalScrollRatio = maxScrollLeft / (metrics.horizontalWrapperWidth - scrollbarWidth);
+ } else {
+ cachedHorizontalScrollRatio = 0;
+ }
+
+ // PHASE 3: Write all DOM properties (already in RAF)
verticalWrapper.style.display = isVerticalRequired ? "block" : "none";
horizontalWrapper.style.display = isHorizontalRequired ? "block" : "none";
- });
- };
-
- const computeScrollbarSize = () => {
- // Vertical scrollbar height
- const visibleHeight = bodyContainer.clientHeight;
- const totalHeight = bodyContainer.scrollHeight;
- const wrapperHeight = verticalWrapper.offsetHeight;
-
- let scrollbarHeight = 0;
- if (totalHeight > 0) {
- scrollbarHeight = (visibleHeight / totalHeight) * wrapperHeight;
- }
-
- // Horizontal scrollbar width (use cached references)
- const visibleWidth = table.clientWidth;
- const totalWidth = Math.max(
- header ? header.scrollWidth : 0,
- body ? body.scrollWidth : 0
- );
- const wrapperWidth = horizontalWrapper.offsetWidth;
-
- let scrollbarWidth = 0;
- if (totalWidth > 0) {
- scrollbarWidth = (visibleWidth / totalWidth) * wrapperWidth;
- }
-
- requestAnimationFrame(() => {
verticalScrollbar.style.height = `${scrollbarHeight}px`;
horizontalScrollbar.style.width = `${scrollbarWidth}px`;
+ verticalScrollbar.style.top = `${verticalTop}px`;
+ horizontalScrollbar.style.left = `${horizontalLeft}px`;
});
};
- const updateVerticalScrollbarPosition = () => {
- const maxScrollTop = bodyContainer.scrollHeight - bodyContainer.clientHeight;
- const wrapperHeight = verticalWrapper.offsetHeight;
-
- if (maxScrollTop > 0) {
- const scrollRatio = wrapperHeight / bodyContainer.scrollHeight;
- verticalScrollbar.style.top = `${bodyContainer.scrollTop * scrollRatio}px`;
- }
- };
-
- const updateHorizontalScrollbarPosition = () => {
- // Use cached references
- const totalWidth = Math.max(
- header ? header.scrollWidth : 0,
- body ? body.scrollWidth : 0
- );
- const maxScrollLeft = totalWidth - table.clientWidth;
- const wrapperWidth = horizontalWrapper.offsetWidth;
-
- if (maxScrollLeft > 0 && totalWidth > 0) {
- const scrollRatio = wrapperWidth / totalWidth;
- horizontalScrollbar.style.left = `${table.scrollLeft * scrollRatio}px`;
- }
- };
-
- // Drag management for vertical scrollbar
+ // Consolidated drag management
let isDraggingVertical = false;
- let startYVertical = 0;
- let pendingVerticalScroll = null;
+ let isDraggingHorizontal = false;
+ let dragStartY = 0;
+ let dragStartX = 0;
+ let dragStartScrollTop = 0;
+ let dragStartScrollLeft = 0;
+ // Vertical scrollbar mousedown
verticalScrollbar.addEventListener("mousedown", (e) => {
isDraggingVertical = true;
- startYVertical = e.clientY;
- document.body.style.userSelect = "none";
- verticalScrollbar.classList.add("dt2-dragging");
+ dragStartY = e.clientY;
+ dragStartScrollTop = cachedBodyScrollTop;
+ wrapper.setAttribute("mf-no-tooltip", "");
});
+ // Horizontal scrollbar mousedown
+ horizontalScrollbar.addEventListener("mousedown", (e) => {
+ isDraggingHorizontal = true;
+ dragStartX = e.clientX;
+ dragStartScrollLeft = cachedTableScrollLeft;
+ wrapper.setAttribute("mf-no-tooltip", "");
+ });
+
+ // Consolidated mousemove listener
document.addEventListener("mousemove", (e) => {
if (isDraggingVertical) {
- const deltaY = e.clientY - startYVertical;
- startYVertical = e.clientY;
-
- // OPTIMIZATION: Store the scroll delta, update visual in RAF
- if (pendingVerticalScroll === null) {
- pendingVerticalScroll = 0;
- }
- pendingVerticalScroll += deltaY;
+ const deltaY = e.clientY - dragStartY;
if (!rafScheduledVertical) {
rafScheduledVertical = true;
requestAnimationFrame(() => {
rafScheduledVertical = false;
-
- const wrapperHeight = verticalWrapper.offsetHeight;
- const scrollbarHeight = verticalScrollbar.offsetHeight;
- const maxScrollTop = bodyContainer.scrollHeight - bodyContainer.clientHeight;
- const scrollRatio = maxScrollTop / (wrapperHeight - scrollbarHeight);
-
- let newTop = parseFloat(verticalScrollbar.style.top || "0") + pendingVerticalScroll;
- newTop = Math.max(0, Math.min(newTop, wrapperHeight - scrollbarHeight));
-
- verticalScrollbar.style.top = `${newTop}px`;
- bodyContainer.scrollTop = newTop * scrollRatio;
-
- pendingVerticalScroll = null;
+ const scrollDelta = deltaY * cachedVerticalScrollRatio;
+ bodyContainer.scrollTop = dragStartScrollTop + scrollDelta;
+ cachedBodyScrollTop = bodyContainer.scrollTop;
+ updateScrollbars();
});
}
- }
- });
-
- document.addEventListener("mouseup", () => {
- if (isDraggingVertical) {
- isDraggingVertical = false;
- document.body.style.userSelect = "";
- verticalScrollbar.classList.remove("dt2-dragging");
- }
- });
-
- // Drag management for horizontal scrollbar
- let isDraggingHorizontal = false;
- let startXHorizontal = 0;
- let pendingHorizontalScroll = null;
-
- horizontalScrollbar.addEventListener("mousedown", (e) => {
- isDraggingHorizontal = true;
- startXHorizontal = e.clientX;
- document.body.style.userSelect = "none";
- horizontalScrollbar.classList.add("dt2-dragging");
- });
-
- document.addEventListener("mousemove", (e) => {
- if (isDraggingHorizontal) {
- const deltaX = e.clientX - startXHorizontal;
- startXHorizontal = e.clientX;
-
- // OPTIMIZATION: Store the scroll delta, update visual in RAF
- if (pendingHorizontalScroll === null) {
- pendingHorizontalScroll = 0;
- }
- pendingHorizontalScroll += deltaX;
+ } else if (isDraggingHorizontal) {
+ const deltaX = e.clientX - dragStartX;
if (!rafScheduledHorizontal) {
rafScheduledHorizontal = true;
requestAnimationFrame(() => {
rafScheduledHorizontal = false;
-
- const wrapperWidth = horizontalWrapper.offsetWidth;
- const scrollbarWidth = horizontalScrollbar.offsetWidth;
-
- // Use cached references
- const totalWidth = Math.max(
- header ? header.scrollWidth : 0,
- body ? body.scrollWidth : 0
- );
- const maxScrollLeft = totalWidth - table.clientWidth;
- const scrollRatio = maxScrollLeft / (wrapperWidth - scrollbarWidth);
-
- let newLeft = parseFloat(horizontalScrollbar.style.left || "0") + pendingHorizontalScroll;
- newLeft = Math.max(0, Math.min(newLeft, wrapperWidth - scrollbarWidth));
-
- horizontalScrollbar.style.left = `${newLeft}px`;
- table.scrollLeft = newLeft * scrollRatio;
-
- pendingHorizontalScroll = null;
+ const scrollDelta = deltaX * cachedHorizontalScrollRatio;
+ table.scrollLeft = dragStartScrollLeft + scrollDelta;
+ cachedTableScrollLeft = table.scrollLeft;
+ updateScrollbars();
});
}
}
});
+ // Consolidated mouseup listener
document.addEventListener("mouseup", () => {
- if (isDraggingHorizontal) {
+ if (isDraggingVertical) {
+ isDraggingVertical = false;
+ wrapper.removeAttribute("mf-no-tooltip");
+ } else if (isDraggingHorizontal) {
isDraggingHorizontal = false;
- document.body.style.userSelect = "";
- horizontalScrollbar.classList.remove("dt2-dragging");
+ wrapper.removeAttribute("mf-no-tooltip");
}
});
@@ -1711,13 +1715,16 @@ function initDataGridScrollbars(gridId) {
bodyContainer.scrollTop += pendingWheelDeltaY;
table.scrollLeft += pendingWheelDeltaX;
- // Update scrollbar positions
- updateVerticalScrollbarPosition();
- updateHorizontalScrollbarPosition();
+ // Update caches with clamped values (read back from DOM in RAF - OK)
+ cachedBodyScrollTop = bodyContainer.scrollTop;
+ cachedTableScrollLeft = table.scrollLeft;
// Reset pending deltas
pendingWheelDeltaX = 0;
pendingWheelDeltaY = 0;
+
+ // Update all scrollbars in a single batched operation
+ updateScrollbars();
});
}
@@ -1726,20 +1733,19 @@ function initDataGridScrollbars(gridId) {
wrapper.addEventListener("wheel", handleWheelScrolling, { passive: false });
- // Initialize scrollbars
- computeScrollbarVisibility();
- computeScrollbarSize();
- updateVerticalScrollbarPosition();
- updateHorizontalScrollbarPosition();
+ // Initialize scrollbars with single batched update
+ updateScrollbars();
- // Recompute on window resize
+ // Recompute on window resize with RAF throttling
+ let resizeScheduled = false;
window.addEventListener("resize", () => {
- computeScrollbarVisibility();
- computeScrollbarSize();
- updateVerticalScrollbarPosition();
- updateHorizontalScrollbarPosition();
+ if (!resizeScheduled) {
+ resizeScheduled = true;
+ requestAnimationFrame(() => {
+ resizeScheduled = false;
+ updateScrollbars();
+ });
+ }
});
-
- console.info(`DataGrid "${gridId}" initialized with custom scrollbars`);
}