Memory leak/Detached DOM nodes when node is removed
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.
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
Does adding a key attribute to the looped elements help?
Does adding a
keyattribute 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
Does adding a
keyattribute 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 }
}
}
Ah I didn't know that's possible - but no, it doesn't help
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!" }
}
}
}
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
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.
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
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.
Same issue here, the memory usage keeps increasing
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
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.
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)
Yeah my use case was virtual scrolling as well. Glad to hear the patch works. I'll try to complete the PR
Hello guys !
Works like a charm with dioxus = { git = "https://github.com/gammahead/dioxus" }
Thanks a lot.