Version: 7.9.5
Mac (Apple Silicon)
Both PHP and NodeJS projects
I have raised this issue before but seemingly been ignored every time (as I think it was conflated into other memory leaks/slow downs that you have cleaned up). The issue still persists, though. I have some pages which, after just a few minutes editing, can take two to three minutes to respond after each keystroke so I don't even both in Wappler (the whole Wappler UI freezes so I can't even do something else). The common denominator of the pages where I see this is the collapse elements (via accordion or collapse) - and seems especially problematic when there is nested collapses e.g. a collapse inside an accordion
To help identify/resolve it, I put the following prompt in to both Gemini and Claude:
When you have a page that has multiple accordion/collapses (especially when they are nested) the whole of Wappler freezes while the page refresh takes place. I put this prompt
Is there anything in this, when combined with Bootstrap 5 collapse, that would cause slow-down in Monaco editor / preview (in Wappler UI) when the page has multiple collapses/accordions (some may be nested)?
dmx.Component("bs5-collapse",{initialData:{collapsed:!0},attributes:{show:{type:Boolean,default:!1}},methods:{toggle(){this._instance.toggle()},show(){this._instance.show()},hide(){this._instance.hide()}},events:{show:Event,shown:Event,hide:Event,hidden:Event},init(s){s.classList.add("collapse"),s.addEventListener("show.bs.collapse",this.dispatchEvent.bind(this,"show")),s.addEventListener("shown.bs.collapse",this.dispatchEvent.bind(this,"shown")),s.addEventListener("hide.bs.collapse",this.dispatchEvent.bind(this,"hide")),s.addEventListener("hidden.bs.collapse",this.dispatchEvent.bind(this,"hidden")),s.addEventListener("shown.bs.collapse",this._shownHandler.bind(this)),s.addEventListener("hidden.bs.collapse",this._hiddenHandler.bind(this));const t={toggle:!1};s.hasAttribute("data-bs-parent")&&(t.parent=s.getAttribute("data-bs-parent")),this._instance=new bootstrap.Collapse(s,t),this._instance[this.props.show?"show":"hide"]()},destroy(){this._instance.dispose()},performUpdate(s){s.has("show")&&(this._instance[this.props.show?"show":"hide"](),this.set("collapsed",!this.props.show))},_shownHandler(){this.set("collapsed",!1)},_hiddenHandler(){this.set("collapsed",!0)}});
to see what they came back with and both had similar themes around handling of event listeners (that seem like potentially easy fixes)
Gemini:
Yes, there are a few architectural things happening in this custom component registration (dmx.Component for Wappler's App Connect framework) that can cause a severe performance bottleneck inside a design-time environment like Wappler's UI / Monaco Preview when dealing with multiple or nested collapses.
Here is exactly what is likely causing the lag:
1. The Real-Time Initialization Loop (The Biggest Culprit)
Look closely at the init(s) function:
this._instance = new bootstrap.Collapse(s, t);
this._instance[this.props.show ? "show" : "hide"]();
When Wappler parses and renders pages in its Design View or App Connect Preview, it constantly updates, tears down, and re-initializes components as you move elements, change properties, or switch files.
If you have dozens of collapses, especially nested ones, Wappler's UI is instantiating Bootstrap’s native JavaScript bootstrap.Collapse over and over again. Inside the Monaco/Preview environment, this forces the browser to calculate layout, transitions, and DOM heights repeatedly, locking up the main thread.
2. Event Listener Multipliers & Memory Leaks
In the init block, you are binding 6-7 event listeners per instance:
s.addEventListener("show.bs.collapse", this.dispatchEvent.bind(this, "show"));
// ... and so on
- The
.bind(this)problem: Every time you use.bind(this), it creates a new function reference in memory. - The
destroy()omission: In thedestroy()method, you havethis._instance.dispose();, which cleans up Bootstrap's internal listeners. However, **you are not removing the event listeners you attached manually tos**. - The Result: When Wappler forces a re-render in the preview, the old listeners stay detached but bound in memory, or worse, they pile up on the DOM nodes if the nodes aren't completely wiped. With nested accordions, this memory footprint grows exponentially, causing Monaco and the preview iframe to crawl.
3. Infinite Update Loops (performUpdate)
performUpdate(s) {
s.has("show") && (this._instance[this.props.show ? "show" : "hide"](), this.set("collapsed", !this.props.show))
}
Wappler’s App Connect relies on a reactive key-value store.
- When
showchanges,performUpdatetriggers Bootstrap's.show()or.hide(). - Bootstrap's transition fires, triggering the
shown.bs.collapseevent listener. - Your listener calls
this._shownHandler(), which runsthis.set("collapsed", false). - Updating component state (
this.set) can force Wappler to schedule another component update cycle.
In a nested setup, parent collapses toggling can trigger layout updates on children, which triggers state changes, causing a cascade of dirty-checking cycles that the Wappler preview engine has to process in real time.
How to Fix / Mitigate the Slowdown
To make this snippet friendly for Wappler's UI, you need to add guardrails for design-time rendering and clean up after yourself.
Step 1: Clean up listeners on Destroy
Save your bound handlers or use a single handler so you can remove them when Wappler destroys the component instance.
Step 2: Skip heavy Bootstrap JS initialization in Design View (Optional but Powerful)
If Wappler provides a way to detect the design environment (e.g., checking wappler or dmx.design), you can skip calling new bootstrap.Collapse() if it's only being rendered for the IDE preview, relying on CSS instead.
Optimized Code Structure
Here is a safer way to structure it to avoid memory leaks:
dmx.Component("bs5-collapse", {
initialData: { collapsed: !0 },
attributes: { show: { type: Boolean, default: !1 } },
methods: {
toggle() { this._instance && this._instance.toggle() },
show() { this._instance && this._instance.show() },
hide() { this._instance && this._instance.hide() }
},
events: { show: Event, shown: Event, hide: Event, hidden: Event },
init(s) {
s.classList.add("collapse");
// Save references so they can be explicitly removed later
this._handlers = {
show: this.dispatchEvent.bind(this, "show"),
shown: () => { this.dispatchEvent("shown"); this._shownHandler(); },
hide: this.dispatchEvent.bind(this, "hide"),
hidden: () => { this.dispatchEvent("hidden"); this._hiddenHandler(); }
};
s.addEventListener("show.bs.collapse", this._handlers.show);
s.addEventListener("shown.bs.collapse", this._handlers.shown);
s.addEventListener("hide.bs.collapse", this._handlers.hide);
s.addEventListener("hidden.bs.collapse", this._handlers.hidden);
const t = { toggle: !1 };
if (s.hasAttribute("data-bs-parent")) {
t.parent = s.getAttribute("data-bs-parent");
}
// Initialize Bootstrap
this._instance = new bootstrap.Collapse(s, t);
// Guard clause for initial state to prevent unnecessary layout thrashing
if (this.props.show) {
this._instance.show();
}
},
destroy() {
// 1. Clean up native Bootstrap instance
if (this._instance) {
this._instance.dispose();
}
// 2. Clean up Wappler DOM listeners to stop memory leaks in Monaco Preview
const s = this.$node; // or the element reference saved from init
if (s && this._handlers) {
s.removeEventListener("show.bs.collapse", this._handlers.show);
s.removeEventListener("shown.bs.collapse", this._handlers.shown);
s.removeEventListener("hide.bs.collapse", this._handlers.hide);
s.removeEventListener("hidden.bs.collapse", this._handlers.hidden);
}
},
performUpdate(s) {
if (s.has("show") && this._instance) {
// Prevent updating if the state already matches to break loop cycles
const isShowing = this.props.show;
this._instance[isShowing ? "show" : "hide"]();
this.set("collapsed", !isShowing);
}
},
_shownHandler() { this.set("collapsed", !1) },
_hiddenHandler() { this.set("collapsed", !0) }
});
Claude:
Looking at this component, a few things stand out that could compound into real slowdown, especially with nested accordions in a live-reloading Monaco/Wappler preview:
1. Event listeners are never removed in destroy()
init() registers six listeners per instance:
s.addEventListener("show.bs.collapse", this.dispatchEvent.bind(this,"show"))
s.addEventListener("shown.bs.collapse", this.dispatchEvent.bind(this,"shown"))
s.addEventListener("hide.bs.collapse", this.dispatchEvent.bind(this,"hide"))
s.addEventListener("hidden.bs.collapse", this.dispatchEvent.bind(this,"hidden"))
s.addEventListener("shown.bs.collapse", this._shownHandler.bind(this))
s.addEventListener("hidden.bs.collapse", this._hiddenHandler.bind(this))
destroy() only calls this._instance.dispose() — it never calls removeEventListener. Worse, since .bind() creates a new function reference every time init() runs, there's no way to remove these later even if you tried, because you don't have a stored reference to the original bound function.
If Wappler's live preview re-runs init() on the same nodes during hot-reload/incremental updates (rather than a full iframe reload each time), you get duplicate listeners stacking up on every edit. Each subsequent show.bs.collapse then fires N handlers instead of 1, and N grows with every save. With multiple/nested accordions this multiplies fast — that's the kind of thing that looks like "the editor just gets slower the longer I work on this page."
2. init() unconditionally calls show()/hide()
this._instance[this.props.show ? "show" : "hide"]()
Bootstrap's hide() bails out early if not currently shown, so that path is cheap. But show() forces a synchronous scrollHeight read (layout reflow) to animate height. If several collapse panels default to show:true on a page, each one forces a reflow during init — and if the preview re-inits all components on every keystroke-triggered rebuild, that's repeated forced reflows across every open panel, every save.
3. Nested accordions cascade through data-bs-parent
When one panel opens, Bootstrap auto-closes its siblings via the shared data-bs-parent. Each sibling's hide.bs.collapse/hidden.bs.collapse fires, which runs _hiddenHandler → this.set('collapsed', true) → pushes into DMX's reactive data layer. With deep nesting, one toggle can ripple through several set() calls, each potentially triggering downstream data-bound re-renders in the preview. Not slow in isolation, but it adds up when it's layered on top of #1's leaking listeners.
Bottom line: the biggest suspect for a progressively worsening slowdown during a Monaco editing session is #1 — the missing listener cleanup. It's not something you'd notice with one or two collapses, but with multiple/nested accordions and repeated live-reload cycles, listener count grows unbounded and each event dispatch does more and more redundant work. #2 and #3 are more about per-toggle cost, which matters more as the panel count grows but wouldn't degrade over time the way #1 would.
If you have control over this component, the fix is straightforward: store the bound handler references on this in init() (e.g. this._boundShow = this.dispatchEvent.bind(this,"show")) and call the matching removeEventListener in destroy().