sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.63k stars 4.13k forks source link

Svelte 5: $derived inside component does not get cleaned up causing memory leak #11817

Closed wattsjs closed 4 months ago

wattsjs commented 4 months ago

Describe the bug

I'm not sure if this is my misunderstanding of the DOM + component lifecycle, however if this is not a bug I would say it is a nasty footgun and can lead to fairly large memory leaks if dealing with large data structures.

I am storing some large amount of data loaded from an external API in a class based state, and then running some derivations on this state as needed. This state is fetched for each different page and then updated.

For example something like below:

export class AppState {
    post = $state() as {
        id: number;
        userId: number;
        title: string;
        body: string;
    };

    newData = $derived.by(() => {
        let largeObject = {} as {
            [key: string]: {
                userId: number;
                id: number;
                title: string;
                body: string;
            };
        };
        for (let i = 0; i < 10000; i++) {
            largeObject[`obj${i}`] = this.post;
        }
        return largeObject;
    });
}

If I have this $derived inside of the class itself (or even in the /[id]/+page.svelte), all works fine. But there are some cases in my app that I want to run this derivation inside of a component, for example if I have many items and want to derive some data from each item. When I move the $derived inside of the component, the memory does not clean up properly as I assume it is still referencing the $state from AppState, which exists - although the data has now changed.

For example some Chrome memory allocation timelines, we can see that when the $derived rune is inside of the component the memory does not get released on each page navigation (vertical line), whereas it does when it is in AppState:

Is this intended behaviour? I would like to have the flexibility to add $derived runes inside of my nested components to calculate new state that is only required locally, instead of having to derive it at a higher level and pass it down (which would be tricky when dealing with lists or maps)

Reproduction

https://github.com/wattsjs/svelte-derived-memory-leak

Logs

No response

System Info

System:
    OS: macOS 14.5
    CPU: (12) arm64 Apple M2 Max
    Memory: 23.80 GB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.11.0 - ~/.local/share/mise/installs/node/20.11.0/bin/node
    npm: 10.2.4 - ~/.local/share/mise/installs/node/20.11.0/bin/npm
    pnpm: 9.1.2 - ~/.local/share/mise/installs/pnpm/latest/bin/pnpm
  Browsers:
    Chrome: 125.0.6422.113
    Safari: 17.5
  npmPackages:
    svelte: ^5.0.0-next.1 => 5.0.0-next.143

Severity

blocking an upgrade

wattsjs commented 4 months ago

Sorry I should have mentioned - the reproduction uses the $derived in the component (causing the leak), which when changed from newData to state.newData will behave as expected: https://github.com/wattsjs/svelte-derived-memory-leak/blob/main/src/lib/SomeComponent.svelte#L28

wattsjs commented 4 months ago

Thanks for the quick fix @trueadm 🙏