From 0454fc7fa6327b9100d6fcf0dd82eb45d7697427 Mon Sep 17 00:00:00 2001 From: Matt Yan Date: Sun, 29 Mar 2026 21:36:26 +0900 Subject: [PATCH 1/3] perf: avoid Rc refcount overhead in DomSlot chain traversal Traverse the DynamicDomSlot chain using raw pointers instead of Rc::clone/drop per hop. The chain is transitively kept alive by the borrowed &self, so raw pointer access is sound. --- packages/yew/src/dom_bundle/position.rs | 34 +++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/yew/src/dom_bundle/position.rs b/packages/yew/src/dom_bundle/position.rs index 44f50104589..47e1d71d7cb 100644 --- a/packages/yew/src/dom_bundle/position.rs +++ b/packages/yew/src/dom_bundle/position.rs @@ -196,22 +196,28 @@ impl DynamicDomSlot { } fn with_next_sibling(&self, f: impl FnOnce(Option<&Node>) -> R) -> R { - // we use an iterative approach to traverse a possible long chain for references - // see for example issue #3043 why a recursive call is impossible for large lists in vdom - - // TODO: there could be some data structure that performs better here. E.g. a balanced tree - // with parent pointers come to mind, but they are a bit fiddly to implement in rust - let mut this = self.target.clone(); + // We use an iterative approach to traverse a possible long chain of references. + // See issue #3043 for why a recursive call is impossible for large lists in vdom. + // + // We traverse via raw pointers to avoid Rc refcount overhead (clone + drop) per hop. + // + // SAFETY: All RefCells in the chain are valid for the duration of this traversal: + // - `self.target` (Rc) is alive because `self` is borrowed + // - Each DomSlot::Chained(DynamicDomSlot { target }) in the chain holds a strong Rc to the + // next RefCell, so all links are transitively kept alive + // - Yew is single-threaded and this function does not yield, so no mutable borrow (e.g. + // from reassign()) can occur on any RefCell in the chain during traversal + // - Each RefCell::borrow() is dropped before advancing to the next hop + let mut ptr: *const RefCell = Rc::as_ptr(&self.target); loop { - // v------- borrow lives for this match expression - let next_this = match &this.borrow().variant { + let cell = unsafe { &*ptr }; + let slot_ref = cell.borrow(); + match &slot_ref.variant { DomSlotVariant::Node(ref n) => break f(n.as_ref()), - // We clone an Rc here temporarily, so that we don't have to consume stack - // space. The alternative would be to keep the - // `Ref<'_, DomSlot>` above in some temporary buffer - DomSlotVariant::Chained(ref chain) => chain.target.clone(), - }; - this = next_this; + DomSlotVariant::Chained(ref chain) => { + ptr = Rc::as_ptr(&chain.target); + } + } } } } From c2baa852f0c0203962523994aa1d39f88647e925 Mon Sep 17 00:00:00 2001 From: Matt Yan Date: Mon, 30 Mar 2026 17:33:56 +0900 Subject: [PATCH 2/3] add back the comments --- packages/yew/src/dom_bundle/position.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/yew/src/dom_bundle/position.rs b/packages/yew/src/dom_bundle/position.rs index 47e1d71d7cb..967556667aa 100644 --- a/packages/yew/src/dom_bundle/position.rs +++ b/packages/yew/src/dom_bundle/position.rs @@ -199,6 +199,9 @@ impl DynamicDomSlot { // We use an iterative approach to traverse a possible long chain of references. // See issue #3043 for why a recursive call is impossible for large lists in vdom. // + // TODO: there could be some data structure that performs better here. E.g. a balanced tree + // with parent pointers come to mind, but they are a bit fiddly to implement in rust + // // We traverse via raw pointers to avoid Rc refcount overhead (clone + drop) per hop. // // SAFETY: All RefCells in the chain are valid for the duration of this traversal: From 5f054aed34b1aea03bb4d6b3da5dded72e47a0c4 Mon Sep 17 00:00:00 2001 From: Matt Yan Date: Sat, 18 Apr 2026 12:06:01 +0900 Subject: [PATCH 3/3] fix: clone terminal Node before invoking `f` in DomSlot traversal --- packages/yew/src/dom_bundle/position.rs | 34 +++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/yew/src/dom_bundle/position.rs b/packages/yew/src/dom_bundle/position.rs index 967556667aa..fae736006ce 100644 --- a/packages/yew/src/dom_bundle/position.rs +++ b/packages/yew/src/dom_bundle/position.rs @@ -202,26 +202,34 @@ impl DynamicDomSlot { // TODO: there could be some data structure that performs better here. E.g. a balanced tree // with parent pointers come to mind, but they are a bit fiddly to implement in rust // - // We traverse via raw pointers to avoid Rc refcount overhead (clone + drop) per hop. + // We traverse via raw pointers to avoid Rc refcount overhead (clone + drop) per hop, then + // clone the terminal next-sibling out of the chain before invoking `f`. Invoking `f` with + // no borrow held and no reliance on chain structure keeps the traversal sound: `f` runs + // arbitrary code (panic drop glue, `gloo::console::error`, tracing subscribers) that + // could, in principle, reassign a link in the chain and drop the last strong reference + // to the RefCell we would otherwise still borrow from. // - // SAFETY: All RefCells in the chain are valid for the duration of this traversal: + // SAFETY: All RefCells visited by the loop remain live while we dereference them: // - `self.target` (Rc) is alive because `self` is borrowed // - Each DomSlot::Chained(DynamicDomSlot { target }) in the chain holds a strong Rc to the // next RefCell, so all links are transitively kept alive - // - Yew is single-threaded and this function does not yield, so no mutable borrow (e.g. - // from reassign()) can occur on any RefCell in the chain during traversal + // - Yew is single-threaded and the loop body does not run user code, so no mutable borrow + // (e.g. from reassign()) can occur on any RefCell in the chain during traversal // - Each RefCell::borrow() is dropped before advancing to the next hop - let mut ptr: *const RefCell = Rc::as_ptr(&self.target); - loop { - let cell = unsafe { &*ptr }; - let slot_ref = cell.borrow(); - match &slot_ref.variant { - DomSlotVariant::Node(ref n) => break f(n.as_ref()), - DomSlotVariant::Chained(ref chain) => { - ptr = Rc::as_ptr(&chain.target); + let node: Option = { + let mut ptr: *const RefCell = Rc::as_ptr(&self.target); + loop { + let cell = unsafe { &*ptr }; + let slot_ref = cell.borrow(); + match &slot_ref.variant { + DomSlotVariant::Node(ref n) => break n.clone(), + DomSlotVariant::Chained(ref chain) => { + ptr = Rc::as_ptr(&chain.target); + } } } - } + }; + f(node.as_ref()) } }