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:
Demonthos authored and GitHub committed 2022-09-30 12:03:06 -07:00
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());