From bb9c545eda83dd172bf9526e1126b084fa4afd12 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:00:41 +0000 Subject: [PATCH] perf: prevent layout thrashing in magnifier update loop Refactors `MagnifyGlass.updateMagnifiedView` and `calculateSourceRegion` to accept an optional cached `DOMRect`. This allows `EventHandler` to pass the canvas dimensions it read before updating the glass position, preventing a forced synchronous reflow (Read-Write-Read cycle) that was previously occurring on every mouse move frame. Optimizes the hot path for mouse movement and magnifier updates. --- .jules/bolt.md | 4 ++++ src/magnify-glass/EventHandler.ts | 7 +++++-- src/magnify-glass/MagnifyGlass.ts | 11 +++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 96282e3..4a0a090 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -5,3 +5,7 @@ ## 2024-05-23 - DOM Property Access in Loops **Learning:** Accessing DOM properties like `canvas.width` or `element.style` inside a loop is much slower than accessing a local variable. **Action:** Cache DOM properties into local variables before entering hot loops. + +## 2024-05-23 - Read-Write-Read Layout Thrashing +**Learning:** Writing to the DOM (e.g., `element.style.top = ...`) immediately invalidates the layout. If you subsequently read a layout property (e.g., `getBoundingClientRect()`) in the same frame, the browser must force a synchronous layout recalculation. +**Action:** In event handlers that move elements, Read all necessary dimensions first, then Perform all Writes. If a downstream method (like `updateMagnifiedView`) needs dimensions, pass the cached values instead of re-reading them. diff --git a/src/magnify-glass/EventHandler.ts b/src/magnify-glass/EventHandler.ts index f25a4c8..23624dc 100644 --- a/src/magnify-glass/EventHandler.ts +++ b/src/magnify-glass/EventHandler.ts @@ -259,7 +259,9 @@ export class EventHandler { this.magnifyGlass.state.y = pixelY; this.magnifyGlass.ui.positionGlass(clientX, clientY); - this.magnifyGlass.updateMagnifiedView(); + // Optimization: Pass the cached rect to avoid re-measuring in calculateSourceRegion + // which would cause layout thrashing (Read-Write-Read) + this.magnifyGlass.updateMagnifiedView(rect); } }); } @@ -305,7 +307,8 @@ export class EventHandler { this.magnifyGlass.ui.positionGlass(clientX, clientY); } - this.magnifyGlass.updateMagnifiedView(); + // Optimization: Pass the cached rect + this.magnifyGlass.updateMagnifiedView(rect); } } } diff --git a/src/magnify-glass/MagnifyGlass.ts b/src/magnify-glass/MagnifyGlass.ts index 56aff2b..09a0955 100644 --- a/src/magnify-glass/MagnifyGlass.ts +++ b/src/magnify-glass/MagnifyGlass.ts @@ -239,8 +239,9 @@ export class MagnifyGlass { /** * Update the magnified view. + * @param rect - Optional pre-calculated bounding rect of the canvas to avoid layout thrashing */ - updateMagnifiedView(): void { + updateMagnifiedView(rect?: DOMRect): void { if (!this.state.active || !this.renderer || !this.litegraphCanvas) { return; } @@ -255,7 +256,7 @@ export class MagnifyGlass { this.updateCanvasTransformation(); // Calculate the source region - this.calculateSourceRegion(); + this.calculateSourceRegion(rect); // Schedule the rendering operation if (!this.state.isRenderScheduled) { @@ -321,8 +322,9 @@ export class MagnifyGlass { /** * Calculate the source region for magnification. + * @param cachedRect - Optional pre-calculated bounding rect of the canvas to avoid layout thrashing */ - calculateSourceRegion(): void { + calculateSourceRegion(cachedRect?: DOMRect): void { const cursorPixelX = this.state.x; const cursorPixelY = this.state.y; const canvasScale = this.state.canvasScale; @@ -333,7 +335,8 @@ export class MagnifyGlass { if (!this.litegraphCanvas) return; // Get DPR (Device Pixel Ratio) - relationship between Backing Store pixels and CSS pixels - const rect = this.litegraphCanvas.getBoundingClientRect(); + // Optimization: Use cached rect if provided to avoid forcing a reflow (Read-Write-Read cycle) + const rect = cachedRect || this.litegraphCanvas.getBoundingClientRect(); const dpr = rect.width > 0 ? this.litegraphCanvas.width / rect.width : 1; // Convert cursor Backing pixels to CSS pixels (Screen pixels)