Comfy-Org / Comfy-Org/ComfyUI_frontend

ComfyUI frontend can take >100s to open in Firefox: invokeExtensionsAsync causes massive extension hook fan-out

Open
#17,124 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
2k
Forks
699
Avg merge
1d 7h
Merged PRs (30d)
490

Description

**Observed impact:** 110.36 s frontend node registration in Firefox vs 1.795 s in Chromium on the same large real-world ComfyUI installation.

# Summary

`invokeExtensionsAsync()` currently creates an `async` callback/Promise for every enabled extension before checking whether that extension implements the requested hook:

```ts
return await Promise.all(
extensionStore.enabledExtensions.map(async (ext) => {
if (method in ext) {
// ...
}
})
)
```

For hooks such as `beforeRegisterNodeDef`, which run once per node definition, this creates work proportional to:

`node_count × enabled_extension_count`

even when only a small subset of extensions actually implement the hook.

This is measurable in Chromium and catastrophic in Firefox at high fan-out. A controlled reproducer and a large real-world ComfyUI installation both show that avoiding async wrappers for extensions without the requested hook removes a large amount of unnecessary work.

Current main still contains the `enabledExtensions.map(async ...)`-before-hook-check pattern in:

https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/services/extensionService.ts

`registerNodesFromDefs()` concurrently registers all node definitions, so this dispatch path is multiplied across all node definitions:

https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/scripts/app.ts

# Controlled reproducer

Dependency-free synthetic workload:

- 6,000 node definitions
- 1,000 enabled extensions
- only 10 extensions implement `beforeRegisterNodeDef`
- hook implementation: synchronous no-op
- object-info payload: 2.84 MB
- unlimited node registration concurrency
- fresh browser process/profile per observation

With the current dispatcher, 6,000,000 async callbacks are created for node-hook dispatch, although only 60,000 hook calls are applicable.

Three-run median for the severe case:

| Dispatcher | Firefox | Chromium |
|---|---:|---:|
| Current | 122.669 s | 0.540 s |
| Applicable-hooks-only | 0.217 s | 0.109 s |

Firefox scaling at fixed 6,000 nodes / 10 synchronous hooks:

| Enabled extensions | Firefox median | Chromium median |
|---:|---:|---:|
| 50 | 0.344 s | 0.039 s |
| 200 | 1.040 s | 0.146 s |
| 500 | 20.392 s | 0.292 s |
| 1000 | 122.669 s | 0.540 s |

Large payload size is not required. At 6,000 nodes / 200 extensions / 10 hooks, increasing the response from 2.84 MB to ~151.8 MB changed Firefox registration from 1.040 s to 1.010 s; parsing rose to ~138 ms, but registration did not become catastrophic.

# Real-world installation validation

Large real-world ComfyUI installation:

- 6,110 node definitions
- 642 enabled frontend extensions
- 425 extensions implement `beforeRegisterNodeDef`

Measured `registerNodesFromDefs`:

| Browser / dispatcher | Time |
|---|---:|
| Firefox 155.0.1, current | 110.36 s |
| Firefox 155.0.1, applicable-hooks-only | 61.24 s |
| Chromium 153, current | 1.795 s |
| Chromium 153, applicable-hooks-only | 1.884 s |

The application-side optimization therefore removes ~44.5% of Firefox registration time in this real-world installation, while Chromium remains roughly unchanged at ~1.8 s.

No third-party extension names or user-specific configuration are required to reproduce the controlled case.

This does **not** imply that the remaining Firefox slowdown is a ComfyUI bug. A separate SpiderMonkey/Minor-GC pathology remains after absent-hook wrappers are removed and reproduces independently with synthetic no-op hooks. The application-side issue here is narrower: ComfyUI creates large numbers of async callbacks for extensions that cannot handle the requested hook.

The Firefox-side issue is tracked separately here:
https://bugzilla.mozilla.org/show_bug.cgi?id=2002852#c1

# Candidate implementation direction

The strongest tested direction is to avoid creating an async wrapper for an extension unless the requested hook exists and is callable, while still preserving observable behavior such as:

- result positions
- receiver (`this`)
- arguments
- async completion
- error isolation
- return values
- invocation/read order

A naïve:

```ts
enabledExtensions
.filter((ext) => typeof ext[method] === 'function')
.map(...)
```

was deliberately tested and is **not** a safe drop-in replacement: it changes result indexing, can lose error isolation for throwing getters, and can miss hooks added dynamically by earlier callbacks.

The tested `applicable-hooks-only` prototype retained indexed results and performed hook lookup in invocation order inside error isolation, while only allocating async wrappers for applicable hooks. It passed the harness tests for receiver/arguments, sync and async hooks, return values, throwing/rejecting hooks, missing/non-function hooks, dynamic hook addition, and exactly-once registration.

This is still not claimed as universally compatible. Production review should include setup-attribution behavior (`legacyMenuCompat`), exotic proxies/getters, cross-node ordering, and the frontend test suite.

# Additional diagnostic evidence

Bounding active node registrations also avoids the catastrophic Firefox cliff, but is a weaker application fix:

| Current dispatcher | Firefox |
|---|---:|
| unlimited | 122.669 s |
| pool 4096 | 50.716 s |
| pool 1024 | 3.718 s |
| pool 256 | 3.528 s |
| pool 64 | 3.284 s |

The allocation-reduction approach is substantially faster than a worker pool in the synthetic high-fan-out case and also improves Chromium.

# Browser-engine note

The independent synthetic reproducer also exposes a Firefox engine issue. Current Firefox Nightly 157.0a1 reproduced the same workload at 170.838 s versus Chromium 153 at 0.546 s, with symbolicated stacks dominated by nursery Minor GC tracing the JS microtask queue. That engine issue is being treated separately and should not block removing the unnecessary application-side allocations described here.

Contributor guide

Open the contributing guide

Research direction

Read src/services/extensionService.ts and trace how src/scripts/app.ts calls registerNodesFromDefs(). Run the frontend test suite and review the tested dispatcher behavior. Done means avoiding wrappers for missing hooks while preserving result positions, receiver and arguments, async completion, error isolation, return values, invocation order, and dynamic hook behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.