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

+201 -33
View File
@@ -128,6 +128,59 @@ pub enum DomEdit<'bump> {
root: u64,
},
/// Create a new purely-text node in a template
CreateTextNodeTemplate {
/// The ID the new node should have.
root: u64,
/// The textcontent of the noden
text: &'bump str,
/// If the id of the node must be kept in the refrences
locally_static: bool,
},
/// Create a new purely-element node in a template
CreateElementTemplate {
/// The ID the new node should have.
root: u64,
/// The tagname of the node
tag: &'bump str,
/// If the id of the node must be kept in the refrences
locally_static: bool,
/// If any children of this node must be kept in the references
fully_static: bool,
},
/// Create a new purely-comment node with a given namespace in a template
CreateElementNsTemplate {
/// The ID the new node should have.
root: u64,
/// The namespace of the node
tag: &'bump str,
/// The namespace of the node (like `SVG`)
ns: &'static str,
/// If the id of the node must be kept in the refrences
locally_static: bool,
/// If any children of this node must be kept in the references
fully_static: bool,
},
/// Create a new placeholder node.
/// In most implementations, this will either be a hidden div or a comment node. in a template
/// Always both locally and fully static
CreatePlaceholderTemplate {
/// The ID the new node should have.
root: u64,
},
/// Create a new Event Listener.
NewEventListener {
/// The name of the event to listen for.
@@ -189,6 +242,38 @@ pub enum DomEdit<'bump> {
/// Manually pop a root node from the stack.
PopRoot {},
/// Enter a TemplateRef tree
EnterTemplateRef {
/// The ID of the node to enter.
root: u64,
},
/// Exit a TemplateRef tree
ExitTemplateRef {},
/// Create a refrence to a template node.
CreateTemplateRef {
/// The ID of the new template refrence.
id: u64,
/// The ID of the template the node is refrencing.
template_id: u64,
},
/// Create a new templete.
/// IMPORTANT: When adding nodes to a templete, id's will reset to zero, so they must be allocated on a different stack.
/// It is recommended to use Cow<NativeNode>.
CreateTemplate {
/// The ID of the new template.
id: u64,
},
/// Finish a templete
FinishTemplate {
/// The number of root nodes in the template
len: u32,
},
}
use fxhash::FxHashSet;
@@ -204,8 +289,8 @@ impl<'a> Mutations<'a> {
}
// Navigation
pub(crate) fn push_root(&mut self, root: ElementId) {
let id = root.as_u64();
pub(crate) fn push_root(&mut self, root: impl Into<u64>) {
let id = root.into();
self.edits.push(PushRoot { root: id });
}
@@ -214,18 +299,18 @@ impl<'a> Mutations<'a> {
self.edits.push(PopRoot {});
}
pub(crate) fn replace_with(&mut self, root: ElementId, m: u32) {
let root = root.as_u64();
pub(crate) fn replace_with(&mut self, root: impl Into<u64>, m: u32) {
let root = root.into();
self.edits.push(ReplaceWith { m, root });
}
pub(crate) fn insert_after(&mut self, root: ElementId, n: u32) {
let root = root.as_u64();
pub(crate) fn insert_after(&mut self, root: impl Into<u64>, n: u32) {
let root = root.into();
self.edits.push(InsertAfter { n, root });
}
pub(crate) fn insert_before(&mut self, root: ElementId, n: u32) {
let root = root.as_u64();
pub(crate) fn insert_before(&mut self, root: impl Into<u64>, n: u32) {
let root = root.into();
self.edits.push(InsertBefore { n, root });
}
@@ -234,13 +319,13 @@ impl<'a> Mutations<'a> {
}
// Remove Nodes from the dom
pub(crate) fn remove(&mut self, id: u64) {
self.edits.push(Remove { root: id });
pub(crate) fn remove(&mut self, id: impl Into<u64>) {
self.edits.push(Remove { root: id.into() });
}
// Create
pub(crate) fn create_text_node(&mut self, text: &'a str, id: ElementId) {
let id = id.as_u64();
pub(crate) fn create_text_node(&mut self, text: &'a str, id: impl Into<u64>) {
let id = id.into();
self.edits.push(CreateTextNode { text, root: id });
}
@@ -248,20 +333,68 @@ impl<'a> Mutations<'a> {
&mut self,
tag: &'static str,
ns: Option<&'static str>,
id: ElementId,
id: impl Into<u64>,
) {
let id = id.as_u64();
let id = id.into();
match ns {
Some(ns) => self.edits.push(CreateElementNs { root: id, ns, tag }),
None => self.edits.push(CreateElement { root: id, tag }),
}
}
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
pub(crate) fn create_placeholder(&mut self, id: ElementId) {
let id = id.as_u64();
pub(crate) fn create_placeholder(&mut self, id: impl Into<u64>) {
let id = id.into();
self.edits.push(CreatePlaceholder { root: id });
}
// Create
pub(crate) fn create_text_node_template(
&mut self,
text: &'a str,
id: impl Into<u64>,
locally_static: bool,
) {
let id = id.into();
self.edits.push(CreateTextNodeTemplate {
text,
root: id,
locally_static,
});
}
pub(crate) fn create_element_template(
&mut self,
tag: &'static str,
ns: Option<&'static str>,
id: impl Into<u64>,
locally_static: bool,
fully_static: bool,
) {
let id = id.into();
match ns {
Some(ns) => self.edits.push(CreateElementNsTemplate {
root: id,
ns,
tag,
locally_static,
fully_static,
}),
None => self.edits.push(CreateElementTemplate {
root: id,
tag,
locally_static,
fully_static,
}),
}
}
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
pub(crate) fn create_placeholder_template(&mut self, id: impl Into<u64>) {
let id = id.into();
self.edits.push(CreatePlaceholderTemplate { root: id });
}
// events
pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) {
let Listener {
@@ -270,7 +403,13 @@ impl<'a> Mutations<'a> {
..
} = listener;
let element_id = mounted_node.get().unwrap().as_u64();
let element_id = match mounted_node.get().unwrap() {
GlobalNodeId::TemplateId {
template_ref_id: _,
template_node_id,
} => template_node_id.into(),
GlobalNodeId::VNodeId(id) => id.into(),
};
self.edits.push(NewEventListener {
scope,
@@ -278,39 +417,41 @@ impl<'a> Mutations<'a> {
root: element_id,
});
}
pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: u64) {
self.edits.push(RemoveEventListener { event, root });
pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: impl Into<u64>) {
self.edits.push(RemoveEventListener {
event,
root: root.into(),
});
}
// modify
pub(crate) fn set_text(&mut self, text: &'a str, root: u64) {
pub(crate) fn set_text(&mut self, text: &'a str, root: impl Into<u64>) {
let root = root.into();
self.edits.push(SetText { text, root });
}
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute<'a>, root: u64) {
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute<'a>, root: impl Into<u64>) {
let root = root.into();
let Attribute {
name,
value,
namespace,
..
value, attribute, ..
} = attribute;
self.edits.push(SetAttribute {
field: name,
field: attribute.name,
value: value.clone(),
ns: *namespace,
ns: attribute.namespace,
root,
});
}
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) {
let Attribute {
name, namespace, ..
} = attribute;
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: impl Into<u64>) {
let root = root.into();
let Attribute { attribute, .. } = attribute;
self.edits.push(RemoveAttribute {
name,
ns: *namespace,
name: attribute.name,
ns: attribute.namespace,
root,
});
}
@@ -318,6 +459,33 @@ impl<'a> Mutations<'a> {
pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) {
self.dirty_scopes.insert(scope);
}
pub(crate) fn create_templete(&mut self, id: impl Into<u64>) {
self.edits.push(CreateTemplate { id: id.into() });
}
pub(crate) fn finish_templete(&mut self, len: u32) {
self.edits.push(FinishTemplate { len });
}
pub(crate) fn create_template_ref(&mut self, id: impl Into<u64>, template_id: u64) {
self.edits.push(CreateTemplateRef {
id: id.into(),
template_id,
})
}
pub(crate) fn enter_template_ref(&mut self, id: impl Into<u64>) {
self.edits.push(EnterTemplateRef { root: id.into() });
}
pub(crate) fn exit_template_ref(&mut self) {
if let Some(&DomEdit::EnterTemplateRef { .. }) = self.edits.last() {
self.edits.pop();
} else {
self.edits.push(ExitTemplateRef {});
}
}
}
// refs are only assigned once