mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-21 23:09:39 +00:00
[perf] Optimize widget rendering performance
- Move TYPE_TO_ENUM_MAP outside function to prevent recreation - Add WIDGET_SUPPORT_MAP for O(1) widget type lookups - Add ESSENTIAL_WIDGET_TYPES Set for fast LOD filtering - Refactor NodeWidgets to use single processedWidgets computed property - Eliminate object creation in render loops - Pre-resolve Vue components to avoid runtime lookups These optimizations reduce GC pressure and improve rendering performance for workflows with many widgets.
This commit is contained in:
@@ -4,15 +4,13 @@
|
||||
</div>
|
||||
<div v-else class="lg-node-widgets flex flex-col gap-2">
|
||||
<component
|
||||
:is="getVueComponent(widget)"
|
||||
v-for="(widget, index) in supportedWidgets"
|
||||
:is="widget.vueComponent"
|
||||
v-for="(widget, index) in processedWidgets"
|
||||
:key="`widget-${index}-${widget.name}`"
|
||||
:widget="simplifiedWidget(widget)"
|
||||
:model-value="getWidgetValue(widget)"
|
||||
:widget="widget.simplified"
|
||||
:model-value="widget.value"
|
||||
:readonly="readonly"
|
||||
@update:model-value="
|
||||
(value: unknown) => handleWidgetUpdate(widget, value)
|
||||
"
|
||||
@update:model-value="widget.updateHandler"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,7 +27,10 @@ import type {
|
||||
VueNodeData
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import { LODLevel } from '@/composables/graph/useLOD'
|
||||
import { useWidgetRenderer } from '@/composables/graph/useWidgetRenderer'
|
||||
import {
|
||||
ESSENTIAL_WIDGET_TYPES,
|
||||
useWidgetRenderer
|
||||
} from '@/composables/graph/useWidgetRenderer'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
|
||||
@@ -58,75 +59,64 @@ onErrorCaptured((error) => {
|
||||
|
||||
const nodeInfo = computed(() => props.nodeData || props.node)
|
||||
|
||||
// Get non-hidden widgets
|
||||
const widgets = computed((): SafeWidgetData[] => {
|
||||
interface ProcessedWidget {
|
||||
name: string
|
||||
vueComponent: any
|
||||
simplified: SimplifiedWidget
|
||||
value: WidgetValue
|
||||
updateHandler: (value: unknown) => void
|
||||
}
|
||||
|
||||
const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
const info = nodeInfo.value
|
||||
if (!info?.widgets) return []
|
||||
|
||||
const filtered = (info.widgets as SafeWidgetData[]).filter(
|
||||
(w: SafeWidgetData) => !w.options?.hidden
|
||||
)
|
||||
return filtered
|
||||
})
|
||||
const widgets = info.widgets as SafeWidgetData[]
|
||||
const lodLevel = props.lodLevel
|
||||
const result: ProcessedWidget[] = []
|
||||
|
||||
// Filter widgets based on LOD level and Vue component support
|
||||
const supportedWidgets = computed((): SafeWidgetData[] => {
|
||||
const allWidgets = widgets.value
|
||||
|
||||
// Filter by Vue component support
|
||||
let supported = allWidgets.filter((widget: SafeWidgetData) => {
|
||||
return shouldRenderAsVue(widget)
|
||||
})
|
||||
|
||||
// Apply LOD filtering for reduced detail level
|
||||
if (props.lodLevel === LODLevel.REDUCED) {
|
||||
const essentialTypes = [
|
||||
'combo',
|
||||
'select',
|
||||
'toggle',
|
||||
'boolean',
|
||||
'slider',
|
||||
'number'
|
||||
]
|
||||
supported = supported.filter((widget: SafeWidgetData) => {
|
||||
return essentialTypes.includes(widget.type?.toLowerCase() || '')
|
||||
})
|
||||
} else if (props.lodLevel === LODLevel.MINIMAL) {
|
||||
// No widgets rendered at minimal LOD
|
||||
if (lodLevel === LODLevel.MINIMAL) {
|
||||
return []
|
||||
}
|
||||
|
||||
return supported
|
||||
for (const widget of widgets) {
|
||||
if (widget.options?.hidden) continue
|
||||
if (widget.options?.canvasOnly) continue
|
||||
if (!widget.type) continue
|
||||
if (!shouldRenderAsVue(widget)) continue
|
||||
|
||||
if (
|
||||
lodLevel === LODLevel.REDUCED &&
|
||||
!ESSENTIAL_WIDGET_TYPES.has(widget.type)
|
||||
)
|
||||
continue
|
||||
|
||||
const componentName = getWidgetComponent(widget.type)
|
||||
const vueComponent = widgetTypeToComponent[componentName] || WidgetInputText
|
||||
|
||||
const simplified: SimplifiedWidget = {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
value: widget.value,
|
||||
options: widget.options,
|
||||
callback: widget.callback
|
||||
}
|
||||
|
||||
const updateHandler = (value: unknown) => {
|
||||
if (widget.callback) {
|
||||
widget.callback(value)
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
name: widget.name,
|
||||
vueComponent,
|
||||
simplified,
|
||||
value: widget.value,
|
||||
updateHandler
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// Get Vue component for widget
|
||||
const getVueComponent = (widget: SafeWidgetData) => {
|
||||
const componentName = getWidgetComponent(widget.type)
|
||||
const component = widgetTypeToComponent[componentName]
|
||||
return component || WidgetInputText // Fallback to text input
|
||||
}
|
||||
|
||||
const getWidgetValue = (widget: SafeWidgetData): WidgetValue => {
|
||||
return widget.value
|
||||
}
|
||||
|
||||
const simplifiedWidget = (widget: SafeWidgetData): SimplifiedWidget => {
|
||||
return {
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
value: getWidgetValue(widget),
|
||||
options: widget.options,
|
||||
callback: widget.callback
|
||||
}
|
||||
}
|
||||
|
||||
// Handle widget value updates
|
||||
const handleWidgetUpdate = (widget: SafeWidgetData, value: unknown) => {
|
||||
// Call LiteGraph callback to update the authoritative state
|
||||
// The callback will trigger the chained callback in useGraphNodeManager
|
||||
// which will update the Vue state automatically
|
||||
if (widget.callback) {
|
||||
widget.callback(value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,83 +7,100 @@ import {
|
||||
widgetTypeToComponent
|
||||
} from '@/components/graph/vueWidgets/widgetRegistry'
|
||||
|
||||
/**
|
||||
* Static mapping of LiteGraph widget types to Vue widget component names
|
||||
* Moved outside function to prevent recreation on every call
|
||||
*/
|
||||
const TYPE_TO_ENUM_MAP: Record<string, string> = {
|
||||
// Number inputs
|
||||
number: WidgetType.NUMBER,
|
||||
slider: WidgetType.SLIDER,
|
||||
INT: WidgetType.INT,
|
||||
FLOAT: WidgetType.FLOAT,
|
||||
|
||||
// Text inputs
|
||||
text: WidgetType.STRING,
|
||||
string: WidgetType.STRING,
|
||||
STRING: WidgetType.STRING,
|
||||
|
||||
// Selection
|
||||
combo: WidgetType.COMBO,
|
||||
COMBO: WidgetType.COMBO,
|
||||
|
||||
// Boolean
|
||||
toggle: WidgetType.TOGGLESWITCH,
|
||||
boolean: WidgetType.BOOLEAN,
|
||||
BOOLEAN: WidgetType.BOOLEAN,
|
||||
|
||||
// Multiline text
|
||||
multiline: WidgetType.TEXTAREA,
|
||||
textarea: WidgetType.TEXTAREA,
|
||||
|
||||
// Advanced widgets
|
||||
color: WidgetType.COLOR,
|
||||
COLOR: WidgetType.COLOR,
|
||||
image: WidgetType.IMAGE,
|
||||
IMAGE: WidgetType.IMAGE,
|
||||
file: WidgetType.FILEUPLOAD,
|
||||
FILEUPLOAD: WidgetType.FILEUPLOAD,
|
||||
|
||||
// Button widget
|
||||
button: WidgetType.BUTTON,
|
||||
BUTTON: WidgetType.BUTTON,
|
||||
|
||||
// Text-based widgets that don't have dedicated components yet
|
||||
MARKDOWN: WidgetType.TEXTAREA, // Markdown should use textarea for now
|
||||
customtext: WidgetType.TEXTAREA // Custom text widgets use textarea for multiline
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Pre-computed widget support map for O(1) lookups
|
||||
* Maps widget type directly to boolean for fast shouldRenderAsVue checks
|
||||
*/
|
||||
const WIDGET_SUPPORT_MAP = new Map(
|
||||
Object.entries(TYPE_TO_ENUM_MAP).map(([type, enumValue]) => [
|
||||
type,
|
||||
widgetTypeToComponent[enumValue] !== undefined
|
||||
])
|
||||
)
|
||||
|
||||
export const ESSENTIAL_WIDGET_TYPES = new Set([
|
||||
'combo',
|
||||
'COMBO',
|
||||
'select',
|
||||
'toggle',
|
||||
'boolean',
|
||||
'BOOLEAN',
|
||||
'slider',
|
||||
'number',
|
||||
'INT',
|
||||
'FLOAT'
|
||||
])
|
||||
|
||||
export const useWidgetRenderer = () => {
|
||||
/**
|
||||
* Map LiteGraph widget types to Vue widget component names
|
||||
*/
|
||||
const getWidgetComponent = (widgetType: string): string => {
|
||||
// Map common LiteGraph widget types to our registry enum keys
|
||||
const typeToEnum: Record<string, string> = {
|
||||
// Number inputs
|
||||
number: WidgetType.NUMBER,
|
||||
slider: WidgetType.SLIDER,
|
||||
INT: WidgetType.INT,
|
||||
FLOAT: WidgetType.FLOAT,
|
||||
const enumKey = TYPE_TO_ENUM_MAP[widgetType]
|
||||
|
||||
// Text inputs
|
||||
text: WidgetType.STRING,
|
||||
string: WidgetType.STRING,
|
||||
STRING: WidgetType.STRING,
|
||||
|
||||
// Selection
|
||||
combo: WidgetType.COMBO,
|
||||
COMBO: WidgetType.COMBO,
|
||||
|
||||
// Boolean
|
||||
toggle: WidgetType.TOGGLESWITCH,
|
||||
boolean: WidgetType.BOOLEAN,
|
||||
BOOLEAN: WidgetType.BOOLEAN,
|
||||
|
||||
// Multiline text
|
||||
multiline: WidgetType.TEXTAREA,
|
||||
textarea: WidgetType.TEXTAREA,
|
||||
|
||||
// Advanced widgets
|
||||
color: WidgetType.COLOR,
|
||||
COLOR: WidgetType.COLOR,
|
||||
image: WidgetType.IMAGE,
|
||||
IMAGE: WidgetType.IMAGE,
|
||||
file: WidgetType.FILEUPLOAD,
|
||||
FILEUPLOAD: WidgetType.FILEUPLOAD,
|
||||
|
||||
// Button widget
|
||||
button: WidgetType.BUTTON,
|
||||
BUTTON: WidgetType.BUTTON,
|
||||
|
||||
// Text-based widgets that don't have dedicated components yet
|
||||
MARKDOWN: WidgetType.TEXTAREA, // Markdown should use textarea for now
|
||||
customtext: WidgetType.TEXTAREA // Custom text widgets use textarea for multiline
|
||||
}
|
||||
|
||||
// Get mapped enum key
|
||||
const enumKey = typeToEnum[widgetType]
|
||||
|
||||
// Check if we have a component for this type
|
||||
if (enumKey && widgetTypeToComponent[enumKey]) {
|
||||
return enumKey
|
||||
}
|
||||
|
||||
return WidgetType.STRING // Return enum key for WidgetInputText
|
||||
return WidgetType.STRING
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a widget should be rendered as Vue component
|
||||
*/
|
||||
const shouldRenderAsVue = (widget: {
|
||||
type?: string
|
||||
options?: Record<string, unknown>
|
||||
}): boolean => {
|
||||
// Skip widgets that are marked as canvas-only
|
||||
if (widget.options?.canvasOnly) return false
|
||||
|
||||
// Skip widgets without a type
|
||||
if (!widget.type) return false
|
||||
|
||||
// Get the component type for this widget
|
||||
const enumKey = getWidgetComponent(widget.type)
|
||||
// Check if widget type is explicitly supported
|
||||
const isSupported = WIDGET_SUPPORT_MAP.get(widget.type)
|
||||
if (isSupported !== undefined) return isSupported
|
||||
|
||||
// If we have a component in our registry, render it as Vue
|
||||
return widgetTypeToComponent[enumKey] !== undefined
|
||||
// Fallback: unknown types are rendered as STRING widget
|
||||
return widgetTypeToComponent[WidgetType.STRING] !== undefined
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user