fix: improve painter cursor performance by bypassing Vue reactivity (#9339)

## Summary
Previously painter has node performance issue. 
Use direct DOM manipulation for cursor position updates instead of
reactive refs, and add will-change-transform for GPU layer promotion.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9339-fix-improve-painter-cursor-performance-by-bypassing-Vue-reactivity-3176d73d365081d88b23d26e774cebf5)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2026-03-02 21:18:48 -05:00
committed by GitHub
parent 117448fba4
commit dccf68ddb7
2 changed files with 15 additions and 22 deletions

View File

@@ -28,8 +28,9 @@
/> />
<div <div
v-show="cursorVisible" v-show="cursorVisible"
class="pointer-events-none absolute left-0 top-0 rounded-full border border-black/60 shadow-[0_0_0_1px_rgba(255,255,255,0.8)]" ref="cursorEl"
:style="cursorStyle" class="pointer-events-none absolute left-0 top-0 rounded-full border border-black/60 shadow-[0_0_0_1px_rgba(255,255,255,0.8)] will-change-transform"
:style="cursorSizeStyle"
/> />
</div> </div>
</div> </div>
@@ -281,6 +282,7 @@ const { nodeId } = defineProps<{
const modelValue = defineModel<string>({ default: '' }) const modelValue = defineModel<string>({ default: '' })
const canvasEl = useTemplateRef<HTMLCanvasElement>('canvasEl') const canvasEl = useTemplateRef<HTMLCanvasElement>('canvasEl')
const cursorEl = useTemplateRef<HTMLElement>('cursorEl')
const controlsEl = useTemplateRef<HTMLDivElement>('controlsEl') const controlsEl = useTemplateRef<HTMLDivElement>('controlsEl')
const { width: controlsWidth } = useElementSize(controlsEl) const { width: controlsWidth } = useElementSize(controlsEl)
const compact = computed( const compact = computed(
@@ -296,8 +298,6 @@ const {
backgroundColor, backgroundColor,
canvasWidth, canvasWidth,
canvasHeight, canvasHeight,
cursorX,
cursorY,
cursorVisible, cursorVisible,
displayBrushSize, displayBrushSize,
inputImageUrl, inputImageUrl,
@@ -309,7 +309,7 @@ const {
handlePointerLeave, handlePointerLeave,
handleInputImageLoad, handleInputImageLoad,
handleClear handleClear
} = usePainter(nodeId, { canvasEl, modelValue }) } = usePainter(nodeId, { canvasEl, cursorEl, modelValue })
const canvasContainerStyle = computed(() => ({ const canvasContainerStyle = computed(() => ({
aspectRatio: `${canvasWidth.value} / ${canvasHeight.value}`, aspectRatio: `${canvasWidth.value} / ${canvasHeight.value}`,
@@ -318,16 +318,10 @@ const canvasContainerStyle = computed(() => ({
: backgroundColor.value : backgroundColor.value
})) }))
const cursorStyle = computed(() => { const cursorSizeStyle = computed(() => ({
const size = displayBrushSize.value width: `${displayBrushSize.value}px`,
const x = cursorX.value - size / 2 height: `${displayBrushSize.value}px`
const y = cursorY.value - size / 2 }))
return {
width: `${size}px`,
height: `${size}px`,
transform: `translate(${x}px, ${y}px)`
}
})
const brushOpacityPercent = computed({ const brushOpacityPercent = computed({
get: () => Math.round(brushOpacity.value * 100), get: () => Math.round(brushOpacity.value * 100),

View File

@@ -27,11 +27,12 @@ export const PAINTER_TOOLS: Record<string, PainterTool> = {
interface UsePainterOptions { interface UsePainterOptions {
canvasEl: Ref<HTMLCanvasElement | null> canvasEl: Ref<HTMLCanvasElement | null>
cursorEl: Ref<HTMLElement | null>
modelValue: Ref<string> modelValue: Ref<string>
} }
export function usePainter(nodeId: string, options: UsePainterOptions) { export function usePainter(nodeId: string, options: UsePainterOptions) {
const { canvasEl, modelValue } = options const { canvasEl, cursorEl, modelValue } = options
const { t } = useI18n() const { t } = useI18n()
const nodeOutputStore = useNodeOutputStore() const nodeOutputStore = useNodeOutputStore()
const toastStore = useToastStore() const toastStore = useToastStore()
@@ -41,8 +42,6 @@ export function usePainter(nodeId: string, options: UsePainterOptions) {
const canvasWidth = ref(512) const canvasWidth = ref(512)
const canvasHeight = ref(512) const canvasHeight = ref(512)
const cursorX = ref(0)
const cursorY = ref(0)
const cursorVisible = ref(false) const cursorVisible = ref(false)
const inputImageUrl = ref<string | null>(null) const inputImageUrl = ref<string | null>(null)
@@ -518,8 +517,10 @@ export function usePainter(nodeId: string, options: UsePainterOptions) {
} }
function updateCursorPos(e: PointerEvent) { function updateCursorPos(e: PointerEvent) {
cursorX.value = e.offsetX const el = cursorEl.value
cursorY.value = e.offsetY if (!el) return
const size = displayBrushSize.value
el.style.transform = `translate(${e.offsetX - size / 2}px, ${e.offsetY - size / 2}px)`
} }
function handlePointerDown(e: PointerEvent) { function handlePointerDown(e: PointerEvent) {
@@ -760,8 +761,6 @@ export function usePainter(nodeId: string, options: UsePainterOptions) {
canvasWidth, canvasWidth,
canvasHeight, canvasHeight,
cursorX,
cursorY,
cursorVisible, cursorVisible,
displayBrushSize, displayBrushSize,