mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 13:12:10 +00:00
feat: vue nodes LOD system (#5631)
## Summary Replaced reactive (Vue-based) widget LOD with CSS visibility control. Performance doesn't dramatically improve, but we avoid the mount/unmount overhead during zoom/pan operations. This PR implements the visual component of LOD—complex widgets that need lifecycle management will be addressed separately. ### Problem & Solution Problem: we want LOD to improve rendering performance and visual feedback but discovered using reactivity in the current setup for it meant mounting/unmounting caused worse lag than the performance it aimed to fix. Switching to render all the details all the time but using css visibility proved to be the best solution. However, it doesn't improve rendering performance by much because the GPU texture size is the bottleneck (from TransformPane.vue CSS transforms) and not rasterization. Solution: Keep all nodes/widgets mounted, use CSS visibility: hidden for LOD. Trade memory for performance stability during zoom/pan/drag operations. ### Technical Decision We chose Performance > Memory: - CSS transforms create a single GPU texture whose size depends on node count, not widget complexity - Mounting/unmounting hundreds of widgets during zoom = noticeable lag from Vue VDOM diffing (since all components are mounted all the time because of viewport culling challenge/trade off see https://github.com/Comfy-Org/ComfyUI_frontend/pull/5510.) - CSS visibility changes = no reactivity overhead, smooth interactions - Result: Similar performance, but without interaction stutters This is the visual layer only. If we want a hook into the LOD state per node / widget that would be the next follow up system to implement. ### Next Steps (maybe) - Chunked (split up single Transform Pane transform layer) when rendering 1000+ nodes (maybe) - ~~Selective unmounting API for widgets that register as "expensive"~~ - ~~Client bound hydration system~~ ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> <img width="1355" height="960" alt="image" src="https://github.com/user-attachments/assets/41474d1b-9dbe-4240-a8cf-f4c9ff51d8e0" /> <img width="1354" height="963" alt="image" src="https://github.com/user-attachments/assets/9f55edaa-5858-41b9-b6a8-c2d37e1649bd" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5631-feat-vue-nodes-LOD-system-2726d73d365081c6a6c4e14aa634f19c) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
bypassed,
|
||||
'will-change-transform': isDragging
|
||||
},
|
||||
lodCssClass,
|
||||
|
||||
shouldHandleNodePointerEvents
|
||||
? 'pointer-events-auto'
|
||||
: 'pointer-events-none'
|
||||
@@ -48,10 +48,9 @@
|
||||
</template>
|
||||
<!-- Header only updates on title/color changes -->
|
||||
<NodeHeader
|
||||
v-memo="[nodeData.title, lodLevel, isCollapsed]"
|
||||
v-memo="[nodeData.title, isCollapsed]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
:collapsed="isCollapsed"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleHeaderTitleUpdate"
|
||||
@@ -60,9 +59,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
(isMinimalLOD || isCollapsed) && executing && progress !== undefined
|
||||
"
|
||||
v-if="isCollapsed && executing && progress !== undefined"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-x-4 -bottom-[1px] translate-y-1/2 rounded-full',
|
||||
@@ -72,7 +69,7 @@
|
||||
:style="{ width: `${Math.min(progress * 100, 100)}%` }"
|
||||
/>
|
||||
|
||||
<template v-if="!isMinimalLOD && !isCollapsed">
|
||||
<template v-if="!isCollapsed">
|
||||
<div class="mb-4 relative">
|
||||
<div :class="separatorClasses" />
|
||||
<!-- Progress bar for executing state -->
|
||||
@@ -96,28 +93,24 @@
|
||||
>
|
||||
<!-- Slots only rendered at full detail -->
|
||||
<NodeSlots
|
||||
v-if="shouldRenderSlots"
|
||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length, lodLevel]"
|
||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
/>
|
||||
|
||||
<!-- Widgets rendered at reduced+ detail -->
|
||||
<NodeWidgets
|
||||
v-if="shouldShowWidgets"
|
||||
v-memo="[nodeData.widgets?.length, lodLevel]"
|
||||
v-if="nodeData.widgets?.length"
|
||||
v-memo="[nodeData.widgets?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
/>
|
||||
|
||||
<!-- Custom content at reduced+ detail -->
|
||||
<NodeContent
|
||||
v-if="shouldShowContent"
|
||||
v-if="hasCustomContent"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:lod-level="lodLevel"
|
||||
:image-urls="nodeImageUrls"
|
||||
/>
|
||||
<!-- Live preview image -->
|
||||
@@ -152,7 +145,6 @@ import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/compo
|
||||
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
|
||||
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
@@ -180,8 +172,7 @@ interface LGraphNodeProps {
|
||||
const {
|
||||
nodeData,
|
||||
error = null,
|
||||
readonly = false,
|
||||
zoomLevel = 1
|
||||
readonly = false
|
||||
} = defineProps<LGraphNodeProps>()
|
||||
|
||||
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeSelect } =
|
||||
@@ -218,18 +209,6 @@ const bypassed = computed((): boolean => nodeData.mode === 4)
|
||||
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
||||
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()
|
||||
|
||||
// LOD (Level of Detail) system based on zoom level
|
||||
const {
|
||||
lodLevel,
|
||||
shouldRenderWidgets,
|
||||
shouldRenderSlots,
|
||||
shouldRenderContent,
|
||||
lodCssClass
|
||||
} = useLOD(() => zoomLevel)
|
||||
|
||||
// Computed properties for template usage
|
||||
const isMinimalLOD = computed(() => lodLevel.value === LODLevel.MINIMAL)
|
||||
|
||||
// Error boundary implementation
|
||||
const renderError = ref<string | null>(null)
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
@@ -271,28 +250,17 @@ const hasCustomContent = computed(() => {
|
||||
})
|
||||
|
||||
// Computed classes and conditions for better reusability
|
||||
const separatorClasses = cn(
|
||||
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full'
|
||||
)
|
||||
const progressClasses = cn('h-2 bg-primary-500 transition-all duration-300')
|
||||
const separatorClasses =
|
||||
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full lod-toggle'
|
||||
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
|
||||
|
||||
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
() => nodeData.id,
|
||||
{
|
||||
isMinimalLOD,
|
||||
isCollapsed
|
||||
}
|
||||
)
|
||||
|
||||
// Common condition computations to avoid repetition
|
||||
const shouldShowWidgets = computed(
|
||||
() => shouldRenderWidgets.value && nodeData.widgets?.length
|
||||
)
|
||||
|
||||
const shouldShowContent = computed(
|
||||
() => shouldRenderContent.value && hasCustomContent.value
|
||||
)
|
||||
|
||||
const borderClass = computed(() => {
|
||||
if (hasAnyError.value) {
|
||||
return 'border-error'
|
||||
|
||||
Reference in New Issue
Block a user