Skip to content

usePusherChannel logs every websocket event to the console, retaining payloads in long-lived tabs #2

Description

@EgorGruzdev

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);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions