Summary
usePusherChannel installs a listenToAll handler that console.logs every incoming websocket event. On a dashboard that stays open for days this retains every logged payload — the console keeps references to the logged objects, so they can never be garbage collected.
Affected code
resources/js/composables/usePusherChannel.ts
export function usePusherChannel(channel: string) {
const instance = window.Echo.channel(channel);
onBeforeMount(() => {
instance.listenToAll((event: string, data: unknown) => {
console.log(event, data); // <-- every event, unconditionally
});
});
onBeforeUnmount(() => {
window.Echo.leaveChannel(channel);
});
return instance;
}
Used by both pages the package ships: resources/js/pages/Dashboard.vue and resources/js/pages/Personal.vue.
Why it matters
This looks like leftover debug instrumentation. Dashboard.vue is meant to be left open permanently as a monitoring view, and its event stream is continuous, so retention grows without bound for as long as the tab lives. The effect is worst if DevTools has been opened on that tab at least once, but Chrome keeps a console buffer regardless.
Downstream this forces consumers to periodically force-reload the page just to reclaim memory, which is expensive because the dashboard is not cheap to render.
Suggested fix
Drop the listenToAll logging, or guard it behind a debug flag (import.meta.env.DEV, a package config option, or similar).
Second observation: DOM nodes accumulate while the row count stays constant
Measured on a live Dashboard page — 66 s window, ~83 operator rows, low activity:
|
start |
end |
| DOM nodes |
1410 |
1546 (+124/min) |
TR (rows) |
83 |
83 |
TD (cells) |
492 |
492 |
SPAN |
369 |
474 |
I (icons) |
193 |
214 |
BUTTON |
109 |
130 |
| JS heap |
43 ↔ 97 MB (sawtooth, GC reclaims) |
|
The table skeleton is stable — TR/TD never change. All growth is inside the cells, i.e. within <TicketPool> rendered by TicketRow.vue. After a full page reload the node count dropped back to the initial value, which suggests the extra nodes were not backed by server state.
I could not rule out that this is simply legitimate inflow of tickets into the pool during the measurement window — worth confirming with a longer measurement on an idle instance.
Checked and ruled out as causes:
TicketRepository.close calls destroy(uuid), so closed tickets are removed from the store.
- The Echo channel count stays constant;
usePusherChannel does leaveChannel in onBeforeUnmount.
Possibly related
Dashboard.vue registers 19 channel.listen(...) handlers in onMounted, while onUnmounted only removes the two router.on(...) listeners. On a normal unmount leaveChannel should drop them, but the first registered handler is
.listen(Events.Common.PropsInvalidated, () => router.reload())
which re-creates the component. Worth verifying the ordering of leaveChannel against the new onMounted so subscriptions cannot stack across reloads.
Environment
ttbooking/ticket-allocator dev-master @ d1e09e1
- Vue 3, Vuetify, pinia-orm,
laravel-echo 2.3.7, pusher-js 8.5.0
- Chrome 151, Windows / macOS
How it was measured
Injected a sampler into the running page and read it back after ~1 min:
setInterval(() => {
__leak.push({
mb: performance.memory.usedJSHeapSize / 1048576,
dom: document.getElementsByTagName('*').length,
});
}, 5000);
Summary
usePusherChannelinstalls alistenToAllhandler thatconsole.logs every incoming websocket event. On a dashboard that stays open for days this retains every logged payload — the console keeps references to the logged objects, so they can never be garbage collected.Affected code
resources/js/composables/usePusherChannel.tsUsed by both pages the package ships:
resources/js/pages/Dashboard.vueandresources/js/pages/Personal.vue.Why it matters
This looks like leftover debug instrumentation.
Dashboard.vueis meant to be left open permanently as a monitoring view, and its event stream is continuous, so retention grows without bound for as long as the tab lives. The effect is worst if DevTools has been opened on that tab at least once, but Chrome keeps a console buffer regardless.Downstream this forces consumers to periodically force-reload the page just to reclaim memory, which is expensive because the dashboard is not cheap to render.
Suggested fix
Drop the
listenToAlllogging, or guard it behind a debug flag (import.meta.env.DEV, a package config option, or similar).Second observation: DOM nodes accumulate while the row count stays constant
Measured on a live
Dashboardpage — 66 s window, ~83 operator rows, low activity:TR(rows)TD(cells)SPANI(icons)BUTTONThe table skeleton is stable —
TR/TDnever change. All growth is inside the cells, i.e. within<TicketPool>rendered byTicketRow.vue. After a full page reload the node count dropped back to the initial value, which suggests the extra nodes were not backed by server state.I could not rule out that this is simply legitimate inflow of tickets into the pool during the measurement window — worth confirming with a longer measurement on an idle instance.
Checked and ruled out as causes:
TicketRepository.closecallsdestroy(uuid), so closed tickets are removed from the store.usePusherChanneldoesleaveChannelinonBeforeUnmount.Possibly related
Dashboard.vueregisters 19channel.listen(...)handlers inonMounted, whileonUnmountedonly removes the tworouter.on(...)listeners. On a normal unmountleaveChannelshould drop them, but the first registered handler iswhich re-creates the component. Worth verifying the ordering of
leaveChannelagainst the newonMountedso subscriptions cannot stack across reloads.Environment
ttbooking/ticket-allocatordev-master@d1e09e1laravel-echo2.3.7,pusher-js8.5.0How it was measured
Injected a sampler into the running page and read it back after ~1 min: