[backport core/1.40] fix: cache canvas cursor style to avoid redundant DOM writes (#9604)

Backport of #9171 to `core/1.40`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9604-backport-core-1-40-fix-cache-canvas-cursor-style-to-avoid-redundant-DOM-writes-31d6d73d36508107ad71fc0ced9541e7)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
Comfy Org PR Bot
2026-03-08 11:39:14 +09:00
committed by GitHub
parent 3b766ac98c
commit 44d0159c82
3 changed files with 73 additions and 2 deletions

View File

@@ -11,6 +11,7 @@ import { forEachNode } from '@/utils/graphTraversalUtil'
import { CanvasPointer } from './CanvasPointer'
import type { ContextMenu } from './ContextMenu'
import { createCursorCache } from './cursorCache'
import { DragAndScale } from './DragAndScale'
import type { AnimationOptions } from './DragAndScale'
import type { LGraph } from './LGraph'
@@ -363,6 +364,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.canvas.dispatchEvent(new CustomEvent(type, { detail }))
}
private _setCursor!: ReturnType<typeof createCursorCache>
private _updateCursorStyle() {
if (!this.state.shouldSetCursor) return
@@ -385,7 +388,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
cursor = 'grab'
}
this.canvas.style.cursor = cursor
this._setCursor(cursor)
}
// Whether the canvas was previously being dragged prior to pressing space key.
@@ -1910,6 +1913,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.pointer.element = element
if (!element) return
this._setCursor = createCursorCache(element)
// TODO: classList.add
element.className += ' lgraphcanvas'
@@ -2969,7 +2973,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// Set appropriate cursor for resize direction
this.canvas.style.cursor = cursors[resizeDirection]
this._setCursor(cursors[resizeDirection])
return
}
}

View File

@@ -0,0 +1,59 @@
import { describe, expect, it, vi } from 'vitest'
import { createCursorCache } from './cursorCache'
function createMockElement() {
let cursorValue = ''
const setter = vi.fn((value: string) => {
cursorValue = value
})
const element = document.createElement('div')
Object.defineProperty(element.style, 'cursor', {
get: () => cursorValue,
set: setter
})
return { element, setter }
}
describe('createCursorCache', () => {
it('should only write to DOM when cursor value changes', () => {
const { element, setter } = createMockElement()
const setCursor = createCursorCache(element)
setCursor('crosshair')
setCursor('crosshair')
setCursor('crosshair')
expect(setter).toHaveBeenCalledTimes(1)
expect(setter).toHaveBeenCalledWith('crosshair')
})
it('should write to DOM when cursor value differs', () => {
const { element, setter } = createMockElement()
const setCursor = createCursorCache(element)
setCursor('default')
setCursor('crosshair')
setCursor('grabbing')
expect(setter).toHaveBeenCalledTimes(3)
expect(setter).toHaveBeenNthCalledWith(1, 'default')
expect(setter).toHaveBeenNthCalledWith(2, 'crosshair')
expect(setter).toHaveBeenNthCalledWith(3, 'grabbing')
})
it('should skip repeated values interspersed with changes', () => {
const { element, setter } = createMockElement()
const setCursor = createCursorCache(element)
setCursor('default')
setCursor('default')
setCursor('grab')
setCursor('grab')
setCursor('default')
expect(setter).toHaveBeenCalledTimes(3)
})
})

View File

@@ -0,0 +1,8 @@
export function createCursorCache(element: HTMLElement) {
let lastCursor = ''
return function setCursor(cursor: string) {
if (cursor === lastCursor) return
lastCursor = cursor
element.style.cursor = cursor
}
}