mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 22:59:14 +00:00
feat: code splitting optimization - reduce initial bundle by 36% (#8542)
## Summary Reduces initial module preload from 12.94 MB to 8.24 MB (-4.7 MB, -36%). ## Changes - Split vendor chunks for better cache isolation (firebase, sentry, i18n, zod, etc.) - Exclude heavy optional chunks from initial preload: THREE.js, xterm, tiptap, chart.js, yjs - Lazy load 16 dialog components in dialogService - Add \endor-yjs\ chunk for CRDT library ## Metrics | Metric | Before | After | |--------|--------|-------| | Initial preload | 12.94 MB | 8.24 MB | | vendor-other | 4,006 KB | 2,156 KB | | dialogService | 1,860 KB | 1,304 KB | All excluded chunks still load on-demand when their features are used. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8542-feat-code-splitting-optimization-reduce-initial-bundle-by-36-2fb6d73d36508146aaf7fdaed3274033) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -121,8 +121,9 @@ const mutationObserver = ref<MutationObserver | null>(null)
|
||||
|
||||
const isStandaloneMode = !props.node && props.modelUrl
|
||||
|
||||
// Use sync version since useLoad3dViewer is already imported (module is loaded)
|
||||
const viewer = props.node
|
||||
? useLoad3dService().getOrCreateViewer(toRaw(props.node))
|
||||
? useLoad3dService().getOrCreateViewerSync(toRaw(props.node), useLoad3dViewer)
|
||||
: useLoad3dViewer()
|
||||
|
||||
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
|
||||
|
||||
@@ -219,7 +219,9 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
// Lazy-loaded to avoid pulling THREE.js into the main bundle
|
||||
const Load3dViewerContent = () =>
|
||||
import('@/components/load3d/Load3dViewerContent.vue')
|
||||
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
|
||||
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
|
||||
@@ -580,7 +580,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
versionAdded: '1.3.7',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
dialogService.showSettingsDialog()
|
||||
void dialogService.showSettingsDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -829,7 +829,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'About ComfyUI',
|
||||
versionAdded: '1.6.4',
|
||||
function: () => {
|
||||
dialogService.showSettingsDialog('about')
|
||||
void dialogService.showSettingsDialog('about')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -72,7 +72,7 @@ export function useHelpCenter(
|
||||
* Show the node conflict dialog with current conflict data
|
||||
*/
|
||||
const showConflictModal = () => {
|
||||
showNodeConflictDialog({
|
||||
void showNodeConflictDialog({
|
||||
showAfterWhatsNew: true,
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { computed, ref, toValue } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import { SUPPORTED_EXTENSIONS } from '@/extensions/core/load3d/interfaces'
|
||||
import { SUPPORTED_EXTENSIONS } from '@/extensions/core/load3d/constants'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import './groupNodeManage'
|
||||
import './groupOptions'
|
||||
import './imageCompare'
|
||||
import './imageCrop'
|
||||
import './load3d'
|
||||
// load3d and saveMesh are loaded on-demand to defer THREE.js (~1.8MB)
|
||||
// The lazy loader triggers loading when a 3D node is used
|
||||
import './load3dLazy'
|
||||
import './maskeditor'
|
||||
if (!isCloud) {
|
||||
await import('./nodeTemplates')
|
||||
@@ -20,7 +22,7 @@ import './noteNode'
|
||||
import './previewAny'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
import './saveMesh'
|
||||
// saveMesh is loaded on-demand with load3d (see load3dLazy.ts)
|
||||
import './selectionBorder'
|
||||
import './simpleTouchSupport'
|
||||
import './slotDefaults'
|
||||
|
||||
16
src/extensions/core/load3d/constants.ts
Normal file
16
src/extensions/core/load3d/constants.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Load3D constants that don't require THREE.js
|
||||
* This file can be imported without pulling in the entire THREE.js bundle
|
||||
*/
|
||||
|
||||
export const SUPPORTED_EXTENSIONS = new Set([
|
||||
'.gltf',
|
||||
'.glb',
|
||||
'.obj',
|
||||
'.fbx',
|
||||
'.stl',
|
||||
'.spz',
|
||||
'.splat',
|
||||
'.ply',
|
||||
'.ksplat'
|
||||
])
|
||||
@@ -1,11 +1,13 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import { type OBJLoader2Parallel } from 'wwobjloader2'
|
||||
// Use type-only imports to avoid pulling THREE.js into the main bundle
|
||||
// These imports are erased at compile time and don't create runtime dependencies
|
||||
import type * as THREE from 'three'
|
||||
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import type { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
import type { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
import type { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import type { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import type { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import type { OBJLoader2Parallel } from 'wwobjloader2'
|
||||
|
||||
export type MaterialMode =
|
||||
| 'original'
|
||||
@@ -192,15 +194,3 @@ export interface LoaderManagerInterface {
|
||||
dispose(): void
|
||||
loadModel(url: string, originalFileName?: string): Promise<void>
|
||||
}
|
||||
|
||||
export const SUPPORTED_EXTENSIONS = new Set([
|
||||
'.gltf',
|
||||
'.glb',
|
||||
'.obj',
|
||||
'.fbx',
|
||||
'.stl',
|
||||
'.spz',
|
||||
'.splat',
|
||||
'.ply',
|
||||
'.ksplat'
|
||||
])
|
||||
|
||||
53
src/extensions/core/load3dLazy.ts
Normal file
53
src/extensions/core/load3dLazy.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Lazy loader for 3D extensions (Load3D, Preview3D, SaveGLB)
|
||||
*
|
||||
* This module defers loading of THREE.js (~1.8MB) until a 3D node is actually
|
||||
* used in a workflow. The heavy imports are only loaded when:
|
||||
* - A workflow containing 3D nodes is loaded
|
||||
* - A user adds a 3D node from the node menu
|
||||
*/
|
||||
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
const LOAD3D_NODE_TYPES = new Set(['Load3D', 'Preview3D', 'SaveGLB'])
|
||||
|
||||
let load3dExtensionsLoaded = false
|
||||
let load3dExtensionsLoading: Promise<void> | null = null
|
||||
|
||||
/**
|
||||
* Dynamically load the 3D extensions (and THREE.js) on demand
|
||||
*/
|
||||
async function loadLoad3dExtensions(): Promise<void> {
|
||||
if (load3dExtensionsLoaded) return
|
||||
|
||||
if (load3dExtensionsLoading) {
|
||||
return load3dExtensionsLoading
|
||||
}
|
||||
|
||||
load3dExtensionsLoading = (async () => {
|
||||
// Import both extensions - they will self-register via useExtensionService()
|
||||
await Promise.all([import('./load3d'), import('./saveMesh')])
|
||||
load3dExtensionsLoaded = true
|
||||
})()
|
||||
|
||||
return load3dExtensionsLoading
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node type is a 3D node that requires THREE.js
|
||||
*/
|
||||
function isLoad3dNodeType(nodeTypeName: string): boolean {
|
||||
return LOAD3D_NODE_TYPES.has(nodeTypeName)
|
||||
}
|
||||
|
||||
// Register a lightweight extension that triggers lazy loading
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.Load3DLazy',
|
||||
|
||||
async beforeRegisterNodeDef(_nodeType, nodeData) {
|
||||
// When a 3D node type is being registered, load the 3D extensions
|
||||
if (isLoad3dNodeType(nodeData.name)) {
|
||||
await loadLoad3dExtensions()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -24,7 +24,7 @@ export function useSubscriptionActions() {
|
||||
})
|
||||
|
||||
const handleAddApiCredits = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
void dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
|
||||
@@ -11,7 +11,8 @@ import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { extractWorkflowFromAsset } from '@/platform/workflow/utils/workflowExtractionUtil'
|
||||
import ImagePreview from '@/renderer/extensions/linearMode/ImagePreview.vue'
|
||||
import Preview3d from '@/renderer/extensions/linearMode/Preview3d.vue'
|
||||
// Lazy-loaded to avoid pulling THREE.js into the main bundle
|
||||
const Preview3d = () => import('@/renderer/extensions/linearMode/Preview3d.vue')
|
||||
import VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'
|
||||
import {
|
||||
getMediaType,
|
||||
|
||||
@@ -1,39 +1,62 @@
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
|
||||
import MissingNodesContent from '@/components/dialog/content/MissingNodesContent.vue'
|
||||
import MissingNodesFooter from '@/components/dialog/content/MissingNodesFooter.vue'
|
||||
import MissingNodesHeader from '@/components/dialog/content/MissingNodesHeader.vue'
|
||||
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
|
||||
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import SignInContent from '@/components/dialog/content/SignInContent.vue'
|
||||
import TopUpCreditsDialogContent from '@/components/dialog/content/TopUpCreditsDialogContent.vue'
|
||||
import UpdatePasswordContent from '@/components/dialog/content/UpdatePasswordContent.vue'
|
||||
import ComfyOrgHeader from '@/components/dialog/header/ComfyOrgHeader.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
ShowDialogOptions
|
||||
} from '@/stores/dialogStore'
|
||||
|
||||
import ImportFailedNodeContent from '@/workbench/extensions/manager/components/manager/ImportFailedNodeContent.vue'
|
||||
import ImportFailedNodeFooter from '@/workbench/extensions/manager/components/manager/ImportFailedNodeFooter.vue'
|
||||
import ImportFailedNodeHeader from '@/workbench/extensions/manager/components/manager/ImportFailedNodeHeader.vue'
|
||||
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
|
||||
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
|
||||
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
|
||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||
|
||||
// Type-only imports for ComponentAttrs inference (no runtime cost)
|
||||
import type MissingNodesContent from '@/components/dialog/content/MissingNodesContent.vue'
|
||||
import type MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
|
||||
// Lazy loaders for dialogs - components are loaded on first use
|
||||
const lazyMissingNodesContent = () =>
|
||||
import('@/components/dialog/content/MissingNodesContent.vue')
|
||||
const lazyMissingNodesHeader = () =>
|
||||
import('@/components/dialog/content/MissingNodesHeader.vue')
|
||||
const lazyMissingNodesFooter = () =>
|
||||
import('@/components/dialog/content/MissingNodesFooter.vue')
|
||||
const lazyMissingModelsWarning = () =>
|
||||
import('@/components/dialog/content/MissingModelsWarning.vue')
|
||||
const lazyApiNodesSignInContent = () =>
|
||||
import('@/components/dialog/content/ApiNodesSignInContent.vue')
|
||||
const lazySignInContent = () =>
|
||||
import('@/components/dialog/content/SignInContent.vue')
|
||||
const lazyTopUpCreditsDialogContent = () =>
|
||||
import('@/components/dialog/content/TopUpCreditsDialogContent.vue')
|
||||
const lazyUpdatePasswordContent = () =>
|
||||
import('@/components/dialog/content/UpdatePasswordContent.vue')
|
||||
const lazyComfyOrgHeader = () =>
|
||||
import('@/components/dialog/header/ComfyOrgHeader.vue')
|
||||
const lazySettingDialogHeader = () =>
|
||||
import('@/components/dialog/header/SettingDialogHeader.vue')
|
||||
const lazySettingDialogContent = () =>
|
||||
import('@/platform/settings/components/SettingDialogContent.vue')
|
||||
const lazyImportFailedNodeContent = () =>
|
||||
import('@/workbench/extensions/manager/components/manager/ImportFailedNodeContent.vue')
|
||||
const lazyImportFailedNodeHeader = () =>
|
||||
import('@/workbench/extensions/manager/components/manager/ImportFailedNodeHeader.vue')
|
||||
const lazyImportFailedNodeFooter = () =>
|
||||
import('@/workbench/extensions/manager/components/manager/ImportFailedNodeFooter.vue')
|
||||
const lazyNodeConflictDialogContent = () =>
|
||||
import('@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue')
|
||||
const lazyNodeConflictHeader = () =>
|
||||
import('@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue')
|
||||
const lazyNodeConflictFooter = () =>
|
||||
import('@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue')
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
| 'default'
|
||||
| 'overwrite'
|
||||
@@ -58,9 +81,19 @@ export interface ExecutionErrorDialogInput {
|
||||
export const useDialogService = () => {
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function showLoadWorkflowWarning(
|
||||
async function showLoadWorkflowWarning(
|
||||
props: ComponentAttrs<typeof MissingNodesContent>
|
||||
) {
|
||||
const [
|
||||
{ default: MissingNodesContent },
|
||||
{ default: MissingNodesHeader },
|
||||
{ default: MissingNodesFooter }
|
||||
] = await Promise.all([
|
||||
lazyMissingNodesContent(),
|
||||
lazyMissingNodesHeader(),
|
||||
lazyMissingNodesFooter()
|
||||
])
|
||||
|
||||
dialogStore.showDialog({
|
||||
key: 'global-missing-nodes',
|
||||
headerComponent: MissingNodesHeader,
|
||||
@@ -84,9 +117,10 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showMissingModelsWarning(
|
||||
async function showMissingModelsWarning(
|
||||
props: ComponentAttrs<typeof MissingModelsWarning>
|
||||
) {
|
||||
const { default: MissingModelsWarning } = await lazyMissingModelsWarning()
|
||||
dialogStore.showDialog({
|
||||
key: 'global-missing-models-warning',
|
||||
component: MissingModelsWarning,
|
||||
@@ -94,7 +128,7 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showSettingsDialog(
|
||||
async function showSettingsDialog(
|
||||
panel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
@@ -106,6 +140,14 @@ export const useDialogService = () => {
|
||||
| 'workspace'
|
||||
| 'secrets'
|
||||
) {
|
||||
const [
|
||||
{ default: SettingDialogHeader },
|
||||
{ default: SettingDialogContent }
|
||||
] = await Promise.all([
|
||||
lazySettingDialogHeader(),
|
||||
lazySettingDialogContent()
|
||||
])
|
||||
|
||||
const props = panel ? { props: { defaultPanel: panel } } : undefined
|
||||
|
||||
dialogStore.showDialog({
|
||||
@@ -116,7 +158,15 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showAboutDialog() {
|
||||
async function showAboutDialog() {
|
||||
const [
|
||||
{ default: SettingDialogHeader },
|
||||
{ default: SettingDialogContent }
|
||||
] = await Promise.all([
|
||||
lazySettingDialogHeader(),
|
||||
lazySettingDialogContent()
|
||||
])
|
||||
|
||||
dialogStore.showDialog({
|
||||
key: 'global-settings',
|
||||
headerComponent: SettingDialogHeader,
|
||||
@@ -223,6 +273,9 @@ export const useDialogService = () => {
|
||||
async function showApiNodesSignInDialog(
|
||||
apiNodeNames: string[]
|
||||
): Promise<boolean> {
|
||||
const [{ default: ApiNodesSignInContent }, { default: ComfyOrgHeader }] =
|
||||
await Promise.all([lazyApiNodesSignInContent(), lazyComfyOrgHeader()])
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
dialogStore.showDialog({
|
||||
key: 'api-nodes-signin',
|
||||
@@ -245,6 +298,9 @@ export const useDialogService = () => {
|
||||
}
|
||||
|
||||
async function showSignInDialog(): Promise<boolean> {
|
||||
const [{ default: SignInContent }, { default: ComfyOrgHeader }] =
|
||||
await Promise.all([lazySignInContent(), lazyComfyOrgHeader()])
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-signin',
|
||||
@@ -340,12 +396,15 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showTopUpCreditsDialog(options?: {
|
||||
async function showTopUpCreditsDialog(options?: {
|
||||
isInsufficientCredits?: boolean
|
||||
}) {
|
||||
const { isActiveSubscription } = useSubscription()
|
||||
if (!isActiveSubscription.value) return
|
||||
|
||||
const { default: TopUpCreditsDialogContent } =
|
||||
await lazyTopUpCreditsDialogContent()
|
||||
|
||||
return dialogStore.showDialog({
|
||||
key: 'top-up-credits',
|
||||
component: TopUpCreditsDialogContent,
|
||||
@@ -364,7 +423,10 @@ export const useDialogService = () => {
|
||||
/**
|
||||
* Shows a dialog for updating the current user's password.
|
||||
*/
|
||||
function showUpdatePasswordDialog() {
|
||||
async function showUpdatePasswordDialog() {
|
||||
const [{ default: UpdatePasswordContent }, { default: ComfyOrgHeader }] =
|
||||
await Promise.all([lazyUpdatePasswordContent(), lazyComfyOrgHeader()])
|
||||
|
||||
return dialogStore.showDialog({
|
||||
key: 'global-update-password',
|
||||
component: UpdatePasswordContent,
|
||||
@@ -426,12 +488,22 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showImportFailedNodeDialog(
|
||||
async function showImportFailedNodeDialog(
|
||||
options: {
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
} = {}
|
||||
) {
|
||||
const [
|
||||
{ default: ImportFailedNodeHeader },
|
||||
{ default: ImportFailedNodeFooter },
|
||||
{ default: ImportFailedNodeContent }
|
||||
] = await Promise.all([
|
||||
lazyImportFailedNodeHeader(),
|
||||
lazyImportFailedNodeFooter(),
|
||||
lazyImportFailedNodeContent()
|
||||
])
|
||||
|
||||
const { dialogComponentProps, conflictedPackages } = options
|
||||
|
||||
return dialogStore.showDialog({
|
||||
@@ -463,7 +535,7 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showNodeConflictDialog(
|
||||
async function showNodeConflictDialog(
|
||||
options: {
|
||||
showAfterWhatsNew?: boolean
|
||||
conflictedPackages?: ConflictDetectionResult[]
|
||||
@@ -472,6 +544,16 @@ export const useDialogService = () => {
|
||||
onButtonClick?: () => void
|
||||
} = {}
|
||||
) {
|
||||
const [
|
||||
{ default: NodeConflictHeader },
|
||||
{ default: NodeConflictFooter },
|
||||
{ default: NodeConflictDialogContent }
|
||||
] = await Promise.all([
|
||||
lazyNodeConflictHeader(),
|
||||
lazyNodeConflictFooter(),
|
||||
lazyNodeConflictDialogContent()
|
||||
])
|
||||
|
||||
const {
|
||||
dialogComponentProps,
|
||||
buttonText,
|
||||
|
||||
@@ -1,11 +1,76 @@
|
||||
/**
|
||||
* Load3D Service - provides access to Load3D instances
|
||||
*
|
||||
* This service uses lazy imports to avoid pulling THREE.js into the main bundle.
|
||||
* The nodeToLoad3dMap is accessed lazily - it will only be available after
|
||||
* the load3d extension has been loaded.
|
||||
*/
|
||||
import { toRaw } from 'vue'
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils'
|
||||
|
||||
import { nodeToLoad3dMap } from '@/composables/useLoad3d'
|
||||
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
|
||||
import type Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { Object3D } from 'three'
|
||||
|
||||
// Type for the useLoad3dViewer composable function
|
||||
// Using explicit type to avoid import() type annotations (lint rule)
|
||||
type UseLoad3dViewerFn = (node?: LGraphNode) => {
|
||||
initializeViewer: (containerRef: HTMLElement, source: Load3d) => Promise<void>
|
||||
initializeStandaloneViewer: (
|
||||
containerRef: HTMLElement,
|
||||
modelUrl: string
|
||||
) => Promise<void>
|
||||
cleanup: () => void
|
||||
handleResize: () => void
|
||||
handleMouseEnter: () => void
|
||||
handleMouseLeave: () => void
|
||||
applyChanges: () => Promise<boolean>
|
||||
restoreInitialState: () => void
|
||||
refreshViewport: () => void
|
||||
exportModel: (format: string) => Promise<void>
|
||||
handleBackgroundImageUpdate: (file: File | null) => Promise<void>
|
||||
handleModelDrop: (file: File) => Promise<void>
|
||||
handleSeek: (progress: number) => void
|
||||
needApplyChanges: { value: boolean }
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// Type for SkeletonUtils module
|
||||
type SkeletonUtilsModule = { clone: (source: Object3D) => Object3D }
|
||||
|
||||
// Cache for lazy-loaded modules
|
||||
let cachedNodeToLoad3dMap: Map<LGraphNode, Load3d> | null = null
|
||||
let cachedUseLoad3dViewer: UseLoad3dViewerFn | null = null
|
||||
let cachedSkeletonUtils: SkeletonUtilsModule | null = null
|
||||
|
||||
// Sync accessor - returns null if module not yet loaded
|
||||
function getNodeToLoad3dMapSync(): Map<LGraphNode, Load3d> | null {
|
||||
return cachedNodeToLoad3dMap
|
||||
}
|
||||
|
||||
// Async loader for nodeToLoad3dMap - also caches for sync access
|
||||
async function loadNodeToLoad3dMap(): Promise<Map<LGraphNode, Load3d>> {
|
||||
if (!cachedNodeToLoad3dMap) {
|
||||
const module = await import('@/composables/useLoad3d')
|
||||
cachedNodeToLoad3dMap = module.nodeToLoad3dMap
|
||||
}
|
||||
return cachedNodeToLoad3dMap
|
||||
}
|
||||
|
||||
async function loadUseLoad3dViewer() {
|
||||
if (!cachedUseLoad3dViewer) {
|
||||
const module = await import('@/composables/useLoad3dViewer')
|
||||
cachedUseLoad3dViewer = module.useLoad3dViewer
|
||||
}
|
||||
return cachedUseLoad3dViewer
|
||||
}
|
||||
|
||||
async function loadSkeletonUtils() {
|
||||
if (!cachedSkeletonUtils) {
|
||||
cachedSkeletonUtils = await import('three/examples/jsm/utils/SkeletonUtils')
|
||||
}
|
||||
return cachedSkeletonUtils
|
||||
}
|
||||
|
||||
// Type definitions for Load3D node
|
||||
interface SceneConfig {
|
||||
@@ -30,14 +95,30 @@ export class Load3dService {
|
||||
return Load3dService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Load3d instance for a node (synchronous).
|
||||
* Returns null if the load3d module hasn't been loaded yet.
|
||||
*/
|
||||
getLoad3d(node: LGraphNode): Load3d | null {
|
||||
const rawNode = toRaw(node)
|
||||
const map = getNodeToLoad3dMapSync()
|
||||
if (!map) return null
|
||||
return map.get(rawNode) || null
|
||||
}
|
||||
|
||||
return nodeToLoad3dMap.get(rawNode) || null
|
||||
/**
|
||||
* Get Load3d instance for a node (async, loads module if needed).
|
||||
*/
|
||||
async getLoad3dAsync(node: LGraphNode): Promise<Load3d | null> {
|
||||
const rawNode = toRaw(node)
|
||||
const map = await loadNodeToLoad3dMap()
|
||||
return map.get(rawNode) || null
|
||||
}
|
||||
|
||||
getNodeByLoad3d(load3d: Load3d): LGraphNode | null {
|
||||
for (const [node, instance] of nodeToLoad3dMap) {
|
||||
const map = getNodeToLoad3dMapSync()
|
||||
if (!map) return null
|
||||
for (const [node, instance] of map) {
|
||||
if (instance === load3d) {
|
||||
return node
|
||||
}
|
||||
@@ -47,23 +128,44 @@ export class Load3dService {
|
||||
|
||||
removeLoad3d(node: LGraphNode) {
|
||||
const rawNode = toRaw(node)
|
||||
const map = getNodeToLoad3dMapSync()
|
||||
if (!map) return
|
||||
|
||||
const instance = nodeToLoad3dMap.get(rawNode)
|
||||
const instance = map.get(rawNode)
|
||||
|
||||
if (instance) {
|
||||
instance.remove()
|
||||
|
||||
nodeToLoad3dMap.delete(rawNode)
|
||||
map.delete(rawNode)
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const [node] of nodeToLoad3dMap) {
|
||||
const map = getNodeToLoad3dMapSync()
|
||||
if (!map) return
|
||||
for (const [node] of map) {
|
||||
this.removeLoad3d(node)
|
||||
}
|
||||
}
|
||||
|
||||
getOrCreateViewer(node: LGraphNode) {
|
||||
/**
|
||||
* Get or create viewer (async, loads module if needed).
|
||||
* Use this for initial viewer creation.
|
||||
*/
|
||||
async getOrCreateViewer(node: LGraphNode) {
|
||||
if (!viewerInstances.has(node.id)) {
|
||||
const useLoad3dViewer = await loadUseLoad3dViewer()
|
||||
viewerInstances.set(node.id, useLoad3dViewer(node))
|
||||
}
|
||||
|
||||
return viewerInstances.get(node.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create viewer (sync version).
|
||||
* Only works after useLoad3dViewer has been loaded.
|
||||
* Returns null if module not yet loaded - use async version instead.
|
||||
*/
|
||||
getOrCreateViewerSync(node: LGraphNode, useLoad3dViewer: UseLoad3dViewerFn) {
|
||||
if (!viewerInstances.has(node.id)) {
|
||||
viewerInstances.set(node.id, useLoad3dViewer(node))
|
||||
}
|
||||
@@ -98,6 +200,7 @@ export class Load3dService {
|
||||
}
|
||||
} else {
|
||||
// Use SkeletonUtils.clone for proper skeletal animation support
|
||||
const SkeletonUtils = await loadSkeletonUtils()
|
||||
const modelClone = SkeletonUtils.clone(sourceModel)
|
||||
|
||||
target.getModelManager().currentModel = modelClone
|
||||
@@ -184,7 +287,7 @@ export class Load3dService {
|
||||
}
|
||||
|
||||
async handleViewerClose(node: LGraphNode) {
|
||||
const viewer = useLoad3dService().getOrCreateViewer(node)
|
||||
const viewer = await useLoad3dService().getOrCreateViewer(node)
|
||||
|
||||
if (viewer.needApplyChanges.value) {
|
||||
await viewer.applyChanges()
|
||||
|
||||
@@ -33,7 +33,7 @@ function createImportFailedDialog() {
|
||||
onClose?: () => void
|
||||
) => {
|
||||
if (conflictedPackages && conflictedPackages.length > 0) {
|
||||
showImportFailedNodeDialog({
|
||||
void showImportFailedNodeDialog({
|
||||
conflictedPackages,
|
||||
dialogComponentProps: {
|
||||
onClose
|
||||
|
||||
@@ -153,7 +153,7 @@ export function useManagerState() {
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
dialogService.showSettingsDialog('extension')
|
||||
void dialogService.showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI: {
|
||||
@@ -173,7 +173,7 @@ export function useManagerState() {
|
||||
}
|
||||
// Fallback to extensions panel if not showing toast
|
||||
if (options?.showToastOnLegacyError === false) {
|
||||
dialogService.showSettingsDialog('extension')
|
||||
void dialogService.showSettingsDialog('extension')
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
135
vite.config.mts
135
vite.config.mts
@@ -419,6 +419,31 @@ export default defineConfig({
|
||||
minify: SHOULD_MINIFY,
|
||||
target: 'es2022',
|
||||
sourcemap: GENERATE_SOURCEMAP,
|
||||
// Exclude heavy optional vendor chunks from initial module preload
|
||||
// These chunks are only needed when their features are used (3D, terminal, etc.)
|
||||
modulePreload: {
|
||||
resolveDependencies: (_filename, deps, { hostType }) => {
|
||||
// Only filter for HTML entry points, not for dynamic imports
|
||||
if (hostType !== 'html') return deps
|
||||
|
||||
// Exclude heavy vendor chunks that should be lazy-loaded
|
||||
// - vendor-three: 3D preview (Load3D nodes)
|
||||
// - vendor-xterm: Terminal emulator (logs panel)
|
||||
// - vendor-tiptap: Rich text editor (markdown widgets)
|
||||
// - vendor-chart: Chart.js (stats/monitoring)
|
||||
// - vendor-yjs: CRDT library (layout store, loaded on first graph)
|
||||
const lazyVendors = [
|
||||
'vendor-three',
|
||||
'vendor-xterm',
|
||||
'vendor-tiptap',
|
||||
'vendor-chart',
|
||||
'vendor-yjs'
|
||||
]
|
||||
return deps.filter(
|
||||
(dep) => !lazyVendors.some((vendor) => dep.includes(vendor))
|
||||
)
|
||||
}
|
||||
},
|
||||
rolldownOptions: {
|
||||
treeshake: {
|
||||
manualPureFunctions: [
|
||||
@@ -442,52 +467,100 @@ export default defineConfig({
|
||||
'console.trace'
|
||||
]
|
||||
},
|
||||
experimental: {
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
output: {
|
||||
keepNames: true,
|
||||
codeSplitting: {
|
||||
groups: [
|
||||
// Framework core - highest priority, very stable
|
||||
{
|
||||
name: 'vendor-vue-core',
|
||||
test: /[\\/]node_modules[\\/](vue|@vue|pinia|vue-router)[\\/]/,
|
||||
priority: 20
|
||||
},
|
||||
|
||||
{
|
||||
name: 'vendor-firebase',
|
||||
test: /[\\/]node_modules[\\/](@?firebase|@firebase)[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
{
|
||||
name: 'vendor-sentry',
|
||||
test: /[\\/]node_modules[\\/]@sentry[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
|
||||
// UI component libraries
|
||||
{
|
||||
name: 'vendor-primevue',
|
||||
test: /[\\/]node_modules[\\/](@?primevue|@primeuix)[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-tiptap',
|
||||
test: /[\\/]node_modules[\\/]@tiptap[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-chart',
|
||||
test: /[\\/]node_modules[\\/]chart\.js[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-three',
|
||||
test: /[\\/]node_modules[\\/](three|@sparkjsdev)[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-xterm',
|
||||
test: /[\\/]node_modules[\\/]@xterm[\\/]/,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
name: 'vendor-vue',
|
||||
test: /[\\/]node_modules[\\/](vue|pinia)[\\/]/,
|
||||
priority: 10
|
||||
priority: 15
|
||||
},
|
||||
{
|
||||
name: 'vendor-reka-ui',
|
||||
test: /[\\/]node_modules[\\/]reka-ui[\\/]/,
|
||||
priority: 10
|
||||
priority: 15
|
||||
},
|
||||
|
||||
// Heavy optional features
|
||||
{
|
||||
name: 'vendor-three',
|
||||
test: /[\\/]node_modules[\\/](three|@sparkjsdev)[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
{
|
||||
name: 'vendor-tiptap',
|
||||
test: /[\\/]node_modules[\\/]@tiptap[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
{
|
||||
name: 'vendor-chart',
|
||||
test: /[\\/]node_modules[\\/]chart\.js[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
{
|
||||
name: 'vendor-xterm',
|
||||
test: /[\\/]node_modules[\\/]@xterm[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
{
|
||||
name: 'vendor-yjs',
|
||||
test: /[\\/]node_modules[\\/](yjs|lib0)[\\/]/,
|
||||
priority: 15
|
||||
},
|
||||
|
||||
// Utilities and validation
|
||||
{
|
||||
name: 'vendor-vueuse',
|
||||
test: /[\\/]node_modules[\\/]@vueuse[\\/]/,
|
||||
priority: 12
|
||||
},
|
||||
{
|
||||
name: 'vendor-i18n',
|
||||
test: /[\\/]node_modules[\\/](vue-i18n|@intlify)[\\/]/,
|
||||
priority: 12
|
||||
},
|
||||
{
|
||||
name: 'vendor-zod',
|
||||
test: /[\\/]node_modules[\\/](zod|zod-validation-error)[\\/]/,
|
||||
priority: 12
|
||||
},
|
||||
{
|
||||
name: 'vendor-axios',
|
||||
test: /[\\/]node_modules[\\/]axios[\\/]/,
|
||||
priority: 12
|
||||
},
|
||||
{
|
||||
name: 'vendor-markdown',
|
||||
test: /[\\/]node_modules[\\/](marked|dompurify)[\\/]/,
|
||||
priority: 12
|
||||
},
|
||||
|
||||
// Catch-all for remaining node_modules
|
||||
{
|
||||
name: 'vendor-other',
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
priority: 0
|
||||
priority: 0,
|
||||
minSize: 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user