[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:
bymyself
2025-07-05 21:01:32 -07:00
parent 18854d7d35
commit 555e806f1e
2 changed files with 138 additions and 131 deletions

View File

@@ -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>

View File

@@ -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 {