From 6d5791eb3c5115c5d0623fc39f0c0b2b34e44202 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:57:01 +0000 Subject: [PATCH] perf: hoist getBoundingClientRect out of render loop - Moves `litegraphCanvas.getBoundingClientRect()` call outside the widget loop in `renderHtmlOverlays`. - Reuses the `dpr` calculated at the top level. - Adds regression test `tests/unit/MagnifyGlassRenderOptim.test.ts`. Improves rendering performance by reducing forced reflows during magnification. --- src/magnify-glass/MagnifyGlass.ts | 6 +- tests/unit/MagnifyGlassRenderOptim.test.ts | 131 +++++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 tests/unit/MagnifyGlassRenderOptim.test.ts diff --git a/src/magnify-glass/MagnifyGlass.ts b/src/magnify-glass/MagnifyGlass.ts index 8cdd783..09305af 100644 --- a/src/magnify-glass/MagnifyGlass.ts +++ b/src/magnify-glass/MagnifyGlass.ts @@ -454,10 +454,10 @@ export class MagnifyGlass { if (elementToProcess && (isTextElement || isVideoElement || isImageElement)) { const widgetRect = elementToProcess.getBoundingClientRect(); - const canvasRect = this.litegraphCanvas!.getBoundingClientRect(); + // Optimization: Reuse canvasRect and dpr from top level scope + const canvasRect = rect; - // DPR = backing pixels / CSS pixels - const dpr = canvasRect.width > 0 ? (this.litegraphCanvas!.width / canvasRect.width) : 1; + // DPR is already calculated at function start const currentScale = this.state.canvasScale; const isVirtualZoomMode = currentScale < 0.7; diff --git a/tests/unit/MagnifyGlassRenderOptim.test.ts b/tests/unit/MagnifyGlassRenderOptim.test.ts new file mode 100644 index 0000000..372b8fa --- /dev/null +++ b/tests/unit/MagnifyGlassRenderOptim.test.ts @@ -0,0 +1,131 @@ + +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; + +// Create hoisted mocks +const { appMock } = vi.hoisted(() => { + return { + appMock: { + registerExtension: vi.fn(), + ui: { + settings: { + getSettingValue: vi.fn(), + setSettingValue: vi.fn(), + } + }, + canvas: { + ds: { + scale: 1, + offset: [0, 0] + } + }, + graph: { + _nodes: [] as any[] + } + } + }; +}); + +// Attach to global for files that rely on global variable 'app' +// @ts-ignore +globalThis.app = appMock; + +vi.mock('/scripts/app.js', () => ({ + app: appMock +})); + +// Import after mocks are set up +import { MagnifyGlass } from '../../src/magnify-glass/MagnifyGlass'; + +describe('MagnifyGlass Rendering Optimization', () => { + let magnifyGlass: MagnifyGlass; + let canvasMock: HTMLCanvasElement; + let canvasGetBoundingClientRectSpy: any; + + beforeEach(() => { + // Reset mocks + vi.clearAllMocks(); + + // Reset appMock state + appMock.graph._nodes = []; + + // Setup document body + document.body.innerHTML = ''; + canvasMock = document.getElementById('graph-canvas') as HTMLCanvasElement; + + // Mock getBoundingClientRect + canvasGetBoundingClientRectSpy = vi.spyOn(canvasMock, 'getBoundingClientRect').mockReturnValue({ + x: 0, y: 0, width: 1000, height: 800, top: 0, left: 0, right: 1000, bottom: 800, toJSON: () => {} + }); + + // Initialize MagnifyGlass manually + magnifyGlass = new MagnifyGlass(); + magnifyGlass.litegraphCanvas = canvasMock; + + // Mock UI container + magnifyGlass.ui.htmlOverlayContainer = document.createElement('div'); + + // Setup state + magnifyGlass.state.active = true; + magnifyGlass.state.canvasScale = 1; + magnifyGlass.state.x = 500; + magnifyGlass.state.y = 400; + + // Setup config + magnifyGlass.config.glassSize = 300; + magnifyGlass.config.zoomFactor = 2; + + // Trigger calculateSourceRegion + magnifyGlass.calculateSourceRegion(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + document.body.innerHTML = ''; + }); + + it('should call getBoundingClientRect multiple times (inefficiently) before optimization', () => { + // Setup nodes and widgets + const numNodes = 5; + const widgetsPerNode = 3; + + const nodes = []; + for (let i = 0; i < numNodes; i++) { + const widgets = []; + for (let j = 0; j < widgetsPerNode; j++) { + const el = document.createElement('div'); + const videoEl = document.createElement('video'); + el.appendChild(videoEl); + + widgets.push({ + type: 'custom', + element: el + }); + } + + nodes.push({ + pos: [400, 300], + size: [200, 200], + widgets: widgets + }); + } + + appMock.graph._nodes = nodes; + + // Mock widget getBoundingClientRect + vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({ + x: 450, y: 350, width: 100, height: 100, top: 350, left: 450, right: 550, bottom: 450, toJSON: () => {} + }); + + // Run renderHtmlOverlays + magnifyGlass.renderHtmlOverlays(); + + // Calculate expected calls + // 1 call in setup (calculateSourceRegion) + // 1 call in renderHtmlOverlays (top level) + // 0 calls inside loop (optimization) + const expectedCalls = 2; + + // After optimization, this should pass + expect(canvasGetBoundingClientRectSpy).toHaveBeenCalledTimes(expectedCalls); + }); +});