primefaces / primevue

Next Generation Vue UI Component Library
https://primevue.org
MIT License
10.63k stars 1.24k forks source link

All components: Memory leak #6715

Open YannisJustine opened 2 weeks ago

YannisJustine commented 2 weeks ago

Describe the bug

Memory leak : callbacks in EventBus are never removed

Description

A memory leak issue exists due to the EventBus. Some callback functions registered to the EventBus are never removed, causing unused callbacks to accumulate in memory over time. This leads to increased memory usage, especially in applications where components with registered callbacks are frequently added and removed.

Steps to Reproduce

  1. Go to official website
  2. Navigate to a component page.
  3. Switch to another component page.
  4. Repeat steps 2 and 3 several times.

Before

image

After

image

Expected Behavior

When a component is destroyed or unmounted, the associated callbacks registered to the EventBus should be removed automatically.

Actual Behavior

Callbacks registered with EventBus.on() are never removed, even after the component is unmounted. This causes memory leaks.

Examples of Affected Code

The following code samples demonstrate instances within PrimeVue where callbacks are registered to EventBus without being properly removed, contributing to memory leaks:

BaseDirective.js

In the BaseDirective.js file, the following code snippet shows how config and config.ripple watchers register listeners on PrimeVueService without cleanup:

const handleWatch = (el) => {
    const watchers = el.$instance?.watch;

    // for 'config'
    watchers?.['config']?.call(el.$instance, el.$instance?.$primevueConfig);
    PrimeVueService.on('config:change', ({ newValue, oldValue }) => watchers?.['config']?.call(el.$instance, newValue, oldValue));

    // for 'config.ripple'
    watchers?.['config.ripple']?.call(el.$instance, el.$instance?.$primevueConfig?.ripple);
    PrimeVueService.on('config:ripple:change', ({ newValue, oldValue }) => watchers?.['config.ripple']?.call(el.$instance, newValue, oldValue));
};

Similarly, _loadStyles registers listeners for theme changes but does not remove them:

_loadStyles: (el, binding, vnode) => {
    const config = BaseDirective._getConfig(binding, vnode);
    const useStyleOptions = { nonce: config?.csp?.nonce };

    BaseDirective._loadCoreStyles(el.$instance, useStyleOptions);
    BaseDirective._loadThemeStyles(el.$instance, useStyleOptions);
    BaseDirective._loadScopedThemeStyles(el.$instance, useStyleOptions);

    BaseDirective._themeChangeListener(() => BaseDirective._loadThemeStyles(el.$instance, useStyleOptions));
};

BaseComponent.vue

In BaseComponent.vue, the dt watcher registers a _themeChangeListener that is never removed:

dt: {
    immediate: true,
    handler(newValue) {
        if (newValue) {
            this._loadScopedThemeStyles(newValue);
            this._themeChangeListener(() => this._loadScopedThemeStyles(newValue));
        } else {
            this._unloadScopedThemeStyles();
        }
    }
}

There are additional instances throughout the code with similar issues, where listeners are registered but not removed after component unmounting.

Proposed Solution

Remove callbacks from EventBus when the component is unmounted or destroyed.

This issue is a generalization of the memory leak reported in issue #6223

Reproducer

https://primevue.org

PrimeVue version

4.2.0

Vue version

4.x

Language

TypeScript

Build / Runtime

Vue CLI App

Browser(s)

No response

raizdev commented 2 weeks ago

https://github.com/primefaces/primeuix/blob/main/packages/utils/src/eventbus/index.ts#L35

using slice and map together is slow...

it will not affect the memory leak but this could be better so use handlers.forEach((handler) => handler(evt)); instead.

mertsincan commented 2 weeks ago

Thanks a lot for your report! I'll check and get back to you.

is-paranoia commented 2 weeks ago

I'm seeing the same problem. Version 4.0.7

Removing PrimeVue components causes the leak to disappear or become negligible