fix: prevent infinite node resize loop in Vue mode (#9177)

## Summary

Fix infinite node resize loop in Vue mode where textarea widgets caused
nodes to grow ~33px per frame indefinitely.

## Changes

- **What**: Two feedback loops broken in the LiteGraph↔Vue layout sync:
1. `_arrangeWidgets()` in LiteGraph's draw loop was calling `setSize()`
every frame with its own computed widget height, which disagreed with
Vue's DOM-measured height. Guarded with `!LiteGraph.vueNodesMode`.
2. `useLayoutSync` was calling `setSize()` which triggers the size
setter → writes back to layoutStore with `source=Canvas` →
`handleLayoutChange` updates CSS vars → ResizeObserver fires → loop.
Changed to direct array assignment (matching the existing position sync
pattern).

## Review Focus

- The `_arrangeWidgets` guard: in Vue mode, the DOM/ResizeObserver is
the source of truth for node sizing, so LiteGraph should not grow nodes
via `setSize()`. Verify no Vue-mode features depend on this growth path.
- The `useLayoutSync` change: `liteNode.size[0] = ...` modifies `_size`
via the getter without triggering the setter, avoiding the Canvas-source
bounce. `onResize` is still called. Verify no downstream code relies on
the setter side effects when syncing from layout store.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9177-fix-prevent-infinite-node-resize-loop-in-Vue-mode-3116d73d365081e4ad88f1cfad51df18)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-02-24 13:12:16 -08:00
committed by GitHub
parent 87341f2c6e
commit 2ff14fadc2
2 changed files with 12 additions and 3 deletions

View File

@@ -4187,7 +4187,12 @@ export class LGraphNode
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/2652
// TODO: Move the layout logic before drawing of the node shape, so we don't
// need to trigger extra round of rendering.
if (y > bodyHeight) {
// In Vue mode, the DOM is the source of truth for node sizing — the
// ResizeObserver feeds measurements back to the layout store. Allowing
// LiteGraph to also call setSize() here creates an infinite feedback loop
// (LG grows node → CSS min-height increases → textarea fills extra space →
// ResizeObserver reports larger size → LG grows node again).
if (!LiteGraph.vueNodesMode && y > bodyHeight) {
this.setSize([this.size[0], y])
this.graph.setDirtyCanvas(false, true)
}

View File

@@ -50,8 +50,12 @@ export function useLayoutSync() {
liteNode.size[0] !== layout.size.width ||
liteNode.size[1] !== layout.size.height
) {
// Use setSize() to trigger onResize callback
liteNode.setSize([layout.size.width, layout.size.height])
// Update internal size directly (like position above) to avoid
// the size setter writing back to layoutStore with Canvas source,
// which would create a feedback loop through handleLayoutChange.
liteNode.size[0] = layout.size.width
liteNode.size[1] = layout.size.height
liteNode.onResize?.(liteNode.size)
}
}