feat: use Web Worker for OBJ loading to prevent UI blocking (#7846)

## Summary

- Replace OBJLoader with OBJLoader2Parallel from wwobjloader2
- OBJ parsing now runs in a Web Worker, keeping UI responsive
- Add 100MB file size limit with user-friendly error message

reduce loading time for 97M obj from 9s to 3s

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7843

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7846-feat-use-Web-Worker-for-OBJ-loading-to-prevent-UI-blocking-2df6d73d36508140bea3c87e83d11278)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2026-01-05 14:07:39 -05:00
committed by GitHub
parent 05028894e5
commit a13aa90875
7 changed files with 71 additions and 10 deletions

View File

@@ -34,9 +34,26 @@ class Load3dUtils {
return await resp.json()
}
static readonly MAX_UPLOAD_SIZE_MB = 100
static async uploadFile(file: File, subfolder: string) {
let uploadPath
const fileSizeMB = file.size / 1024 / 1024
if (fileSizeMB > this.MAX_UPLOAD_SIZE_MB) {
const message = t('toastMessages.fileTooLarge', {
size: fileSizeMB.toFixed(1),
maxSize: this.MAX_UPLOAD_SIZE_MB
})
console.warn(
'[Load3D] uploadFile: file too large',
fileSizeMB.toFixed(2),
'MB'
)
useToastStore().addAlert(message)
return undefined
}
try {
const body = new FormData()
body.append('image', file)
@@ -61,7 +78,7 @@ class Load3dUtils {
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
}
} catch (error) {
console.error('Upload error:', error)
console.error('[Load3D] uploadFile: exception', error)
useToastStore().addAlert(
error instanceof Error
? error.message

View File

@@ -3,9 +3,10 @@ import * as THREE from 'three'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { MtlObjBridge, OBJLoader2Parallel } from 'wwobjloader2'
import OBJLoader2WorkerUrl from 'wwobjloader2/worker?url'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -22,7 +23,7 @@ import { FastPLYLoader } from './loader/FastPLYLoader'
export class LoaderManager implements LoaderManagerInterface {
gltfLoader: GLTFLoader
objLoader: OBJLoader
objLoader: OBJLoader2Parallel
mtlLoader: MTLLoader
fbxLoader: FBXLoader
stlLoader: STLLoader
@@ -41,7 +42,12 @@ export class LoaderManager implements LoaderManagerInterface {
this.eventManager = eventManager
this.gltfLoader = new GLTFLoader()
this.objLoader = new OBJLoader()
this.objLoader = new OBJLoader2Parallel()
// Set worker URL for Vite compatibility
this.objLoader.setWorkerUrl(
true,
new URL(OBJLoader2WorkerUrl, import.meta.url)
)
this.mtlLoader = new MTLLoader()
this.fbxLoader = new FBXLoader()
this.stlLoader = new STLLoader()
@@ -173,7 +179,9 @@ export class LoaderManager implements LoaderManagerInterface {
const materials = await this.mtlLoader.loadAsync(mtlFileName)
materials.preload()
this.objLoader.setMaterials(materials)
const materialsFromMtl =
MtlObjBridge.addMaterialsFromMtlLoader(materials)
this.objLoader.setMaterials(materialsFromMtl)
} catch (e) {
console.log(
'No MTL file found or error loading it, continuing without materials'
@@ -181,8 +189,10 @@ export class LoaderManager implements LoaderManagerInterface {
}
}
this.objLoader.setPath(path)
model = await this.objLoader.loadAsync(filename)
// OBJLoader2Parallel uses Web Worker for parsing (non-blocking)
const objUrl = path + encodeURIComponent(filename)
model = await this.objLoader.loadAsync(objUrl)
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
this.modelManager.originalMaterials.set(child, child.material)
@@ -193,7 +203,6 @@ export class LoaderManager implements LoaderManagerInterface {
case 'gltf':
case 'glb':
this.gltfLoader.setPath(path)
const gltf = await this.gltfLoader.loadAsync(filename)
this.modelManager.setOriginalModel(gltf)

View File

@@ -4,8 +4,8 @@ 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 { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { type OBJLoader2Parallel } from 'wwobjloader2'
export type MaterialMode =
| 'original'
@@ -179,7 +179,7 @@ export interface ModelManagerInterface {
export interface LoaderManagerInterface {
gltfLoader: GLTFLoader
objLoader: OBJLoader
objLoader: OBJLoader2Parallel
mtlLoader: MTLLoader
fbxLoader: FBXLoader
stlLoader: STLLoader