From a9f9afd06234d96f410d7dfb2186ac35b99a24d6 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 13 Mar 2026 08:35:03 -0700 Subject: [PATCH] fix: avoid forced layout in renderInfo by using canvas.height (#9304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Replace `canvas.offsetHeight` with `canvas.height / devicePixelRatio` in `renderInfo` to avoid forced synchronous layout. ## Why `renderInfo` is called ~2,631 times in a typical session. Each call reads `this.canvas.offsetHeight`, which forces the browser to flush pending style/layout changes synchronously. With PrimeVue injecting styles dynamically and Vue patching the DOM, there are almost always pending mutations — converting every canvas-only `renderInfo` call into a forced layout. ## How `canvas.height` is the DPR-scaled internal resolution (set in `resizeCanvas` as `cssHeight * devicePixelRatio`). Dividing by `devicePixelRatio` yields the same CSS pixel value as `offsetHeight` without triggering layout. ## Verification - [x] Unit test: verifies `offsetHeight` is not accessed when y is provided - [x] Unit test: verifies fallback uses `canvas.height / devicePixelRatio` - [x] `pnpm typecheck` passes - [x] `pnpm lint` passes - [x] All litegraph tests pass (538 passed) ## Perf Impact Eliminates ~2,631 forced synchronous layouts per session from the canvas info panel. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9304-fix-avoid-forced-layout-in-renderInfo-by-using-canvas-height-3156d73d36508171973dda289b30d5ee) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action --- .../src/LGraphCanvas.renderInfo.test.ts | 61 +++++++++++++++++++ src/lib/litegraph/src/LGraphCanvas.ts | 7 ++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/lib/litegraph/src/LGraphCanvas.renderInfo.test.ts diff --git a/src/lib/litegraph/src/LGraphCanvas.renderInfo.test.ts b/src/lib/litegraph/src/LGraphCanvas.renderInfo.test.ts new file mode 100644 index 0000000000..40cba2d056 --- /dev/null +++ b/src/lib/litegraph/src/LGraphCanvas.renderInfo.test.ts @@ -0,0 +1,61 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { LGraph, LGraphCanvas } from '@/lib/litegraph/src/litegraph' + +describe('LGraphCanvas.renderInfo', () => { + let lgCanvas: LGraphCanvas + let ctx: CanvasRenderingContext2D + + beforeEach(() => { + const canvasElement = document.createElement('canvas') + ctx = { + save: vi.fn(), + restore: vi.fn(), + translate: vi.fn(), + font: '', + fillStyle: '', + textAlign: 'left' as CanvasTextAlign, + fillText: vi.fn() + } as Partial as CanvasRenderingContext2D + + canvasElement.getContext = vi.fn().mockReturnValue(ctx) + + const graph = new LGraph() + lgCanvas = new LGraphCanvas(canvasElement, graph, { + skip_render: true, + skip_events: true + }) + }) + + it('does not access canvas.offsetHeight when y is provided', () => { + const spy = vi.spyOn(lgCanvas.canvas, 'offsetHeight', 'get') + + lgCanvas.renderInfo(ctx, 10, 500) + + expect(spy).not.toHaveBeenCalled() + }) + + it('uses canvas.height divided by devicePixelRatio as y fallback', () => { + lgCanvas.canvas.width = 1920 + lgCanvas.canvas.height = 2160 + + const originalDPR = window.devicePixelRatio + Object.defineProperty(window, 'devicePixelRatio', { + value: 2, + configurable: true + }) + + try { + lgCanvas.renderInfo(ctx, 10, 0) + + // lineCount = 5 (graph present, no info_text), lineHeight = 13 + // y = canvas.height / DPR - (lineCount + 1) * lineHeight + expect(ctx.translate).toHaveBeenCalledWith(10, 2160 / 2 - 6 * 13) + } finally { + Object.defineProperty(window, 'devicePixelRatio', { + value: originalDPR, + configurable: true + }) + } + }) +}) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index f6f2f4355e..d52475a4ed 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -5264,7 +5264,12 @@ export class LGraphCanvas implements CustomEventDispatcher const lineHeight = 13 const lineCount = (this.graph ? 5 : 1) + (this.info_text ? 1 : 0) x = x || 10 - y = y || this.canvas.offsetHeight - (lineCount + 1) * lineHeight + y = + y || + this.canvas.height / + ((this.canvas.ownerDocument.defaultView ?? window).devicePixelRatio || + 1) - + (lineCount + 1) * lineHeight ctx.save() ctx.translate(x, y)