Subtree memorization / reactive templates (#488)
This commit adds subtree memoization to Dioxus. Subtree memoization is basically a compile-time step that drastically reduces the amount of work the diffing engine needs to do at runtime by extracting non-changing nodes out into a static "template." Templates are then understood by the various renderers in the ecosystem as a faster way of rendering the same items. For example, in the web, templates are simply a set of DOM Nodes created once and then cloned later. This is the same pattern frameworks like Lithtml and SolidJS use to achieve near-perfect performance. Subtree memoization adds an additional level of complexity to Dioxus. The RSX macro needs to be much smarter to identify changing/nonchanging nodes and generate a mapping between the Template and its runtime counterparts. This commit represents a working starter point for this work, adding support for templates for the web, desktop, liveview, ssr, and native-core renderers. In the future we will try to shrink code generation, generally improve performance, and simplify our implementation.
This commit is contained in:
1 parent
b32fd2d2cd
commit
047ed1e553
80 files changed
+6774
-2552
No files matched your search
@@ -1,7 +1,8 @@
|
||||
use anymap::AnyMap;
|
||||
use dioxus::core as dioxus_core;
|
||||
use dioxus::core::ElementId;
|
||||
use dioxus::core::{self as dioxus_core, GlobalNodeId};
|
||||
use dioxus::core::{AttributeValue, DomEdit, Mutations};
|
||||
use dioxus::core_macro::rsx_without_templates;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_native_core::node_ref::*;
|
||||
use dioxus_native_core::real_dom::*;
|
||||
@@ -50,14 +51,13 @@ impl ChildDepState for ChildDepCallCounter {
|
||||
const NODE_MASK: NodeMask = NodeMask::ALL;
|
||||
fn reduce<'a>(
|
||||
&mut self,
|
||||
node: NodeView,
|
||||
_: NodeView,
|
||||
_: impl Iterator<Item = &'a Self::DepState>,
|
||||
_: &Self::Ctx,
|
||||
) -> bool
|
||||
where
|
||||
Self::DepState: 'a,
|
||||
{
|
||||
println!("{self:?} {:?}: {} {:?}", node.tag(), node.id(), node.text());
|
||||
self.0 += 1;
|
||||
true
|
||||
}
|
||||
@@ -142,8 +142,16 @@ impl NodeDepState<()> for NodeStateTester {
|
||||
*self = NodeStateTester(
|
||||
node.tag().map(|s| s.to_string()),
|
||||
node.attributes()
|
||||
.map(|a| (a.name.to_string(), a.value.to_string()))
|
||||
.collect(),
|
||||
.map(|iter| {
|
||||
iter.map(|a| {
|
||||
(
|
||||
a.attribute.name.to_string(),
|
||||
format!("{}", a.value.as_text().unwrap()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
true
|
||||
}
|
||||
@@ -185,9 +193,12 @@ fn state_initial() {
|
||||
let nodes_updated = dom.apply_mutations(vec![mutations]);
|
||||
let mut ctx = AnyMap::new();
|
||||
ctx.insert(42u32);
|
||||
let _to_rerender = dom.update_state(&vdom, nodes_updated, ctx);
|
||||
let _to_rerender = dom.update_state(nodes_updated, ctx);
|
||||
|
||||
let root_div = &dom[ElementId(1)];
|
||||
let root_div = &dom[GlobalNodeId::TemplateId {
|
||||
template_ref_id: dioxus_core::ElementId(1),
|
||||
template_node_id: dioxus::prelude::TemplateNodeId(0),
|
||||
}];
|
||||
assert_eq!(root_div.state.bubbled.0, Some("div".to_string()));
|
||||
assert_eq!(
|
||||
root_div.state.bubbled.1,
|
||||
@@ -199,12 +210,18 @@ fn state_initial() {
|
||||
assert_eq!(root_div.state.pushed.0, Some("div".to_string()));
|
||||
assert_eq!(
|
||||
root_div.state.pushed.1,
|
||||
Some(Box::new(PushedDownStateTester(None, None)))
|
||||
Some(Box::new(PushedDownStateTester(
|
||||
Some("Root".to_string()),
|
||||
None
|
||||
)))
|
||||
);
|
||||
assert_eq!(root_div.state.node.0, Some("div".to_string()));
|
||||
assert_eq!(root_div.state.node.1, vec![]);
|
||||
|
||||
let child_p = &dom[ElementId(2)];
|
||||
let child_p = &dom[GlobalNodeId::TemplateId {
|
||||
template_ref_id: dioxus_core::ElementId(1),
|
||||
template_node_id: dioxus::prelude::TemplateNodeId(1),
|
||||
}];
|
||||
assert_eq!(child_p.state.bubbled.0, Some("p".to_string()));
|
||||
assert_eq!(child_p.state.bubbled.1, Vec::new());
|
||||
assert_eq!(child_p.state.pushed.0, Some("p".to_string()));
|
||||
@@ -212,7 +229,10 @@ fn state_initial() {
|
||||
child_p.state.pushed.1,
|
||||
Some(Box::new(PushedDownStateTester(
|
||||
Some("div".to_string()),
|
||||
Some(Box::new(PushedDownStateTester(None, None)))
|
||||
Some(Box::new(PushedDownStateTester(
|
||||
Some("Root".to_string()),
|
||||
None
|
||||
)))
|
||||
)))
|
||||
);
|
||||
assert_eq!(child_p.state.node.0, Some("p".to_string()));
|
||||
@@ -221,7 +241,10 @@ fn state_initial() {
|
||||
vec![("color".to_string(), "red".to_string())]
|
||||
);
|
||||
|
||||
let child_h1 = &dom[ElementId(3)];
|
||||
let child_h1 = &dom[GlobalNodeId::TemplateId {
|
||||
template_ref_id: dioxus_core::ElementId(1),
|
||||
template_node_id: dioxus::prelude::TemplateNodeId(2),
|
||||
}];
|
||||
assert_eq!(child_h1.state.bubbled.0, Some("h1".to_string()));
|
||||
assert_eq!(child_h1.state.bubbled.1, Vec::new());
|
||||
assert_eq!(child_h1.state.pushed.0, Some("h1".to_string()));
|
||||
@@ -229,7 +252,10 @@ fn state_initial() {
|
||||
child_h1.state.pushed.1,
|
||||
Some(Box::new(PushedDownStateTester(
|
||||
Some("div".to_string()),
|
||||
Some(Box::new(PushedDownStateTester(None, None)))
|
||||
Some(Box::new(PushedDownStateTester(
|
||||
Some("Root".to_string()),
|
||||
None
|
||||
)))
|
||||
)))
|
||||
);
|
||||
assert_eq!(child_h1.state.node.0, Some("h1".to_string()));
|
||||
@@ -261,7 +287,7 @@ fn state_reduce_parent_called_minimally_on_update() {
|
||||
|
||||
let vdom = VirtualDom::new(Base);
|
||||
|
||||
let mutations = vdom.create_vnodes(rsx! {
|
||||
let mutations = vdom.create_vnodes(rsx_without_templates! {
|
||||
div {
|
||||
width: "100%",
|
||||
div{
|
||||
@@ -284,7 +310,7 @@ fn state_reduce_parent_called_minimally_on_update() {
|
||||
let mut dom: RealDom<CallCounterState> = RealDom::new();
|
||||
|
||||
let nodes_updated = dom.apply_mutations(vec![mutations]);
|
||||
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
|
||||
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
|
||||
let nodes_updated = dom.apply_mutations(vec![Mutations {
|
||||
edits: vec![DomEdit::SetAttribute {
|
||||
root: 1,
|
||||
@@ -295,7 +321,7 @@ fn state_reduce_parent_called_minimally_on_update() {
|
||||
dirty_scopes: fxhash::FxHashSet::default(),
|
||||
refs: Vec::new(),
|
||||
}]);
|
||||
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
|
||||
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
|
||||
|
||||
dom.traverse_depth_first(|n| {
|
||||
assert_eq!(n.state.part2.parent_counter.0, 2);
|
||||
@@ -307,7 +333,7 @@ fn state_reduce_parent_called_minimally_on_update() {
|
||||
fn state_reduce_child_called_minimally_on_update() {
|
||||
#[allow(non_snake_case)]
|
||||
fn Base(cx: Scope) -> Element {
|
||||
render!(div {
|
||||
cx.render(rsx_without_templates!(div {
|
||||
div{
|
||||
div{
|
||||
p{
|
||||
@@ -324,12 +350,12 @@ fn state_reduce_child_called_minimally_on_update() {
|
||||
"world"
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
let vdom = VirtualDom::new(Base);
|
||||
|
||||
let mutations = vdom.create_vnodes(rsx! {
|
||||
let mutations = vdom.create_vnodes(rsx_without_templates! {
|
||||
div {
|
||||
div{
|
||||
div{
|
||||
@@ -353,7 +379,7 @@ fn state_reduce_child_called_minimally_on_update() {
|
||||
let mut dom: RealDom<CallCounterState> = RealDom::new();
|
||||
|
||||
let nodes_updated = dom.apply_mutations(vec![mutations]);
|
||||
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
|
||||
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
|
||||
let nodes_updated = dom.apply_mutations(vec![Mutations {
|
||||
edits: vec![DomEdit::SetAttribute {
|
||||
root: 4,
|
||||
@@ -364,15 +390,33 @@ fn state_reduce_child_called_minimally_on_update() {
|
||||
dirty_scopes: fxhash::FxHashSet::default(),
|
||||
refs: Vec::new(),
|
||||
}]);
|
||||
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
|
||||
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
|
||||
|
||||
dom.traverse_depth_first(|n| {
|
||||
println!("{:?}", n);
|
||||
assert_eq!(
|
||||
n.state.part1.child_counter.0,
|
||||
if n.id.0 > 4 { 1 } else { 2 }
|
||||
if let GlobalNodeId::VNodeId(ElementId(id)) = n.node_data.id {
|
||||
if id > 4 {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
}
|
||||
} else {
|
||||
panic!()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
n.state.child_counter.0,
|
||||
if let GlobalNodeId::VNodeId(ElementId(id)) = n.node_data.id {
|
||||
if id > 4 {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
}
|
||||
} else {
|
||||
panic!()
|
||||
}
|
||||
);
|
||||
assert_eq!(n.state.child_counter.0, if n.id.0 > 4 { 1 } else { 2 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -457,7 +501,7 @@ fn dependancies_order_independant() {
|
||||
let mut dom: RealDom<UnorderedDependanciesState> = RealDom::new();
|
||||
|
||||
let nodes_updated = dom.apply_mutations(vec![mutations]);
|
||||
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
|
||||
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
|
||||
|
||||
let c = CDepCallCounter(1);
|
||||
let b = BDepCallCounter(1, c.clone());
|
||||
|
||||
Reference in new issue
Block a user