dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Memory leak/Detached DOM nodes when node is removed

Open gammahead opened this issue 10 months ago • 9 comments

Problem

Rendering a Signal<Vec<T>> with something like the following causes unbounded DOM node growth when the Vec<T> can change length up and down between renders. It creates a bunch of detached nodes as can be seen in the image.

Image
rsx! { 
  for item in vec_signal { 
    Child { item }
  }
}

Steps To Reproduce

Steps to reproduce the behavior:

use dioxus::prelude::*;
use uuid::Uuid;
use wasm_bindgen::prelude::wasm_bindgen;
use web_sys::js_sys;

#[wasm_bindgen]
pub fn sleep(ms: i32) -> js_sys::Promise {
    js_sys::Promise::new(&mut |resolve, _| {
        web_sys::window()
            .unwrap()
            .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
            .unwrap();
    })
}

pub async fn shleep(ms: i32) {
    wasm_bindgen_futures::JsFuture::from(sleep(ms))
        .await
        .unwrap();
}

fn main() {
    dioxus::launch(app);
}

fn app() -> Element {
    let mut nums = use_signal(|| vec![]);

    let _tx = use_coroutine(move |_rx: UnboundedReceiver<()>| async move {
        let mut i = 0;
        loop {
            i += 1;
            shleep(100).await;
            if i % 2 == 0 {
                nums.set((i..i + 1).collect());
            } else {
                nums.set((i..i + 2).collect()); // switch this to i + 101 to make the detached nodes grow faster
            }
        }
    });

    rsx! {
        div {
            Child { nums }
        }
    }
}

#[component]
fn Child(nums: Signal<Vec<i32>>) -> Element {
    rsx! {
        for num in nums() {
            div { "Num: {num}" }
        }
    }
}

Expected behavior

Nodes should drop without hanging around in a detached state

Screenshots

Environment:

  • Dioxus version: 0.6.3
  • Rust version: 1.82.0
  • OS info: macOS
  • App platform: web

gammahead avatar Feb 19 '25 03:02 gammahead

Does adding a key attribute to the looped elements help?

marc2332 avatar Feb 19 '25 07:02 marc2332

Does adding a key attribute to the looped elements help?

Does that mean wrapping it with a 'Fragment'? I'm AFK right now, but if that's what you mean, I don't think it makes a difference. I use 'Fragment' in my original code and it has same problem.

I don't know if I'm barking up the wrong tree here but I think it's a node deletion problem because I see detached nodes from other parts of the DOM popping up outside the list as well. I saw some of the logic explicitly wants to keep some state around so maybe it's some combo of trying to reuse elements while adding and deleting same time

gammahead avatar Feb 19 '25 20:02 gammahead

Does adding a key attribute to the looped elements help?

Does that mean wrapping it with a 'Fragment'? I'm AFK right now, but if that's what you mean, I don't think it makes a difference. I use 'Fragment' in my original code and it has same problem.

I don't know if I'm barking up the wrong tree here but I think it's a node deletion problem because I see detached nodes from other parts of the DOM popping up outside the list as well. I saw some of the logic explicitly wants to keep some state around so maybe it's some combo of trying to reuse elements while adding and deleting same time

I mean:

rsx! { 
  for (i, item) in vec_signal.into_iter().enumerate() { 
    Child { key: "{i}", item }
  }
}

marc2332 avatar Feb 19 '25 22:02 marc2332

Ah I didn't know that's possible - but no, it doesn't help

gammahead avatar Feb 20 '25 01:02 gammahead

Actually the issue is more fundamental than a list with varying number of elements. It appears to be a general issue with removing nodes from the DOM. If you simply toggle back and forth between showing an element every second, then you will see a new DOM node get created every 2 seconds as well as a new detached node every other second.

use dioxus::prelude::*;
use uuid::Uuid;
use wasm_bindgen::prelude::wasm_bindgen;
use web_sys::js_sys;

#[wasm_bindgen]
pub fn sleep(ms: i32) -> js_sys::Promise {
    js_sys::Promise::new(&mut |resolve, _| {
        web_sys::window()
            .unwrap()
            .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms)
            .unwrap();
    })
}

pub async fn shleep(ms: i32) {
    wasm_bindgen_futures::JsFuture::from(sleep(ms))
        .await
        .unwrap();
}

fn main() {
    dioxus::launch(app);
}

fn app() -> Element {
    let mut show = use_signal(|| true);

    use_future(move || async move {
        loop {
            shleep(1000).await;
            show.set(!show());
        }
    });

    rsx! {
        if show() {
            div { key: "1", "Hello, world!" }
        }
    }
}

gammahead avatar Feb 20 '25 22:02 gammahead

The dioxus interpreter uses a slot map to store element references. The slots should automatically get reused over time and clear the old references as new nodes are allocated. If they are not getting cleared, this is probably a bug in dioxus core

ealmloff avatar Feb 20 '25 22:02 ealmloff

Just kidding, the new example code is not bad. The number of detached nodes does grow, but they eventually get garbage collected. I'll try isolate under what conditions GC fails to clean up the nodes.

gammahead avatar Feb 20 '25 22:02 gammahead

I believe the problem ultimately boils down to https://github.com/DioxusLabs/dioxus/blob/bdeedc13eb504d3e05edb48575ceda52aea09eff/packages/core/src/diff/node.rs#L335C8-L335C31 calling remove_dynamic_node(to=None, ...), resulting in a noop on the Text(_) | Placeholder(_) branch. The ElementId for the removed node is never reclaimed or reused

gammahead avatar Feb 21 '25 00:02 gammahead

I am using lealeft(more massive js) in a element. use_eval with or without future. When I change my view and back in the element around 10 times, the app totally crash at leaving the map element. I am thinking it s connected to this issue. The crash append on Android virtual or real device.

bennukem avatar Apr 15 '25 23:04 bennukem

Same issue here, the memory usage keeps increasing

iamcco avatar May 03 '25 03:05 iamcco

I had a PR open for this, but I didn't get it over the finish line. @iamcco and/or @bennukem Can you try updating your dioxus dependency to dioxus = { git = "https://github.com/gammahead/dioxus" } and see if your memory issues are resolved? If yes, I will prioritize finishing #3782

gammahead avatar May 03 '25 22:05 gammahead

I had a PR open for this, but I didn't get it over the finish line. @iamcco and/or @bennukem Can you try updating your dioxus dependency to dioxus = { git = "https://github.com/gammahead/dioxus" } and see if your memory issues are resolved? If yes, I will prioritize finishing #3782

wow, it does resolve the issue, the memory does not keep increasing anymore.

iamcco avatar May 04 '25 03:05 iamcco

Same here , my app rewrites the dom almost every frame during worst case tests , it wouldn't work decently without this patch (use case is a virtual/infinite scroll)

HaHa421 avatar May 04 '25 08:05 HaHa421

Yeah my use case was virtual scrolling as well. Glad to hear the patch works. I'll try to complete the PR

gammahead avatar May 04 '25 19:05 gammahead

Hello guys ! Works like a charm with dioxus = { git = "https://github.com/gammahead/dioxus" } Thanks a lot.

bennukem avatar May 04 '25 22:05 bennukem