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

+162 -28
View File
@@ -3,16 +3,70 @@
//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers. These VNodes should be *very*
//! cheap and *very* fast to construct - building a full tree should be quick.
use crate::{
innerlude::{AttributeValue, ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState},
dynamic_template_context::TemplateContext,
innerlude::{
AttributeValue, ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState, Template,
TemplateId,
},
lazynodes::LazyNodes,
template::{TemplateNodeId, VTemplateRef},
AnyEvent, Component,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
num::ParseIntError,
rc::Rc,
str::FromStr,
};
/// The ID of a node in the vdom that is either standalone or in a template
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(untagged))]
pub enum GlobalNodeId {
/// The ID of a node and the template that contains it
TemplateId {
/// The template that contains the node
template_ref_id: ElementId,
/// The ID of the node in the template
template_node_id: TemplateNodeId,
},
/// The ID of a regular node
VNodeId(ElementId),
}
impl Into<GlobalNodeId> for ElementId {
fn into(self) -> GlobalNodeId {
GlobalNodeId::VNodeId(self)
}
}
impl PartialEq<ElementId> for GlobalNodeId {
fn eq(&self, other: &ElementId) -> bool {
match self {
GlobalNodeId::TemplateId { .. } => false,
GlobalNodeId::VNodeId(id) => id == other,
}
}
}
impl FromStr for GlobalNodeId {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((tmpl_id, node_id)) = s.split_once(',') {
Ok(GlobalNodeId::TemplateId {
template_ref_id: ElementId(tmpl_id.parse()?),
template_node_id: TemplateNodeId(node_id.parse()?),
})
} else {
Ok(GlobalNodeId::VNodeId(ElementId(s.parse()?)))
}
}
}
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM.
///
/// VNodes are designed to be lightweight and used with with a bump allocator. To create a VNode, you can use either of:
@@ -112,6 +166,9 @@ pub enum VNode<'src> {
/// }
/// ```
Placeholder(&'src VPlaceholder),
/// Templetes ase generated by the rsx macro to eleminate diffing static nodes.
TemplateRef(&'src VTemplateRef<'src>),
}
impl<'src> VNode<'src> {
@@ -123,6 +180,7 @@ impl<'src> VNode<'src> {
VNode::Fragment(f) => f.key,
VNode::Text(_t) => None,
VNode::Placeholder(_f) => None,
VNode::TemplateRef(t) => t.dynamic_context.key,
}
}
@@ -143,6 +201,7 @@ impl<'src> VNode<'src> {
VNode::Placeholder(el) => el.id.get(),
VNode::Fragment(_) => None,
VNode::Component(_) => None,
VNode::TemplateRef(t) => t.id.get(),
}
}
@@ -154,6 +213,7 @@ impl<'src> VNode<'src> {
VNode::Component(c) => VNode::Component(c),
VNode::Placeholder(a) => VNode::Placeholder(a),
VNode::Fragment(f) => VNode::Fragment(f),
VNode::TemplateRef(t) => VNode::TemplateRef(t),
}
}
}
@@ -189,6 +249,11 @@ impl Debug for VNode<'_> {
.field("key", &comp.key)
.field("scope", &comp.scope)
.finish(),
VNode::TemplateRef(temp) => s
.debug_struct("VNode::TemplateRef")
.field("template_id", &temp.template_id)
.field("id", &temp.id)
.finish(),
}
}
}
@@ -198,6 +263,8 @@ impl Debug for VNode<'_> {
/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
/// unmounted, then the `ElementId` will be reused for a new component.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(transparent))]
pub struct ElementId(pub usize);
impl std::fmt::Display for ElementId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -206,13 +273,19 @@ impl std::fmt::Display for ElementId {
}
impl ElementId {
/// Convertt the ElementId to a `u64`.
pub fn as_u64(self) -> u64 {
/// Convert the ElementId to a `u64`.
pub fn as_u64(&self) -> u64 {
(*self).into()
}
}
impl Into<u64> for ElementId {
fn into(self) -> u64 {
self.0 as u64
}
}
fn empty_cell() -> Cell<Option<ElementId>> {
fn empty_cell<T>() -> Cell<Option<T>> {
Cell::new(None)
}
@@ -267,7 +340,7 @@ pub struct VElement<'a> {
/// The parent of the Element (if any).
///
/// Used when bubbling events
pub parent: Cell<Option<ElementId>>,
pub parent: Cell<Option<GlobalNodeId>>,
/// The Listeners of the VElement.
pub listeners: &'a [Listener<'a>],
@@ -330,30 +403,52 @@ pub trait DioxusElement {
}
}
type StaticStr = &'static str;
/// A discription of the attribute
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(
all(feature = "serialize", any(feature = "hot-reload", debug_assertions)),
derive(serde::Deserialize)
)]
pub struct AttributeDiscription {
/// The name of the attribute.
#[cfg_attr(
all(feature = "serialize", any(feature = "hot-reload", debug_assertions)),
serde(deserialize_with = "crate::util::deserialize_static_leaky")
)]
pub name: StaticStr,
/// The namespace of the attribute.
///
/// Doesn't exist in the html spec.
/// Used in Dioxus to denote "style" tags and other attribute groups.
#[cfg_attr(
all(feature = "serialize", any(feature = "hot-reload", debug_assertions)),
serde(deserialize_with = "crate::util::deserialize_static_leaky_ns")
)]
pub namespace: Option<StaticStr>,
/// An indication of we should always try and set the attribute.
/// Used in controlled components to ensure changes are propagated.
pub volatile: bool,
}
/// An attribute on a DOM node, such as `id="my-thing"` or
/// `href="https://example.com"`.
#[derive(Clone, Debug)]
pub struct Attribute<'a> {
/// The name of the attribute.
pub name: &'static str,
/// The value of the attribute.
pub value: AttributeValue<'a>,
/// The discription of the attribute.
pub attribute: AttributeDiscription,
/// An indication if this attribute can be ignored during diffing
///
/// Usually only when there are no strings to be formatted (so the value is &'static str)
pub is_static: bool,
/// An indication of we should always try and set the attribute.
/// Used in controlled components to ensure changes are propagated.
pub is_volatile: bool,
/// The namespace of the attribute.
///
/// Doesn't exist in the html spec.
/// Used in Dioxus to denote "style" tags and other attribute groups.
pub namespace: Option<&'static str>,
/// The value of the attribute.
pub value: AttributeValue<'a>,
}
/// An event listener.
@@ -361,7 +456,7 @@ pub struct Attribute<'a> {
pub struct Listener<'bump> {
/// The ID of the node that this listener is mounted to
/// Used to generate the event listener's ID on the DOM
pub mounted_node: Cell<Option<ElementId>>,
pub mounted_node: Cell<Option<GlobalNodeId>>,
/// The type of event to listen for.
///
@@ -599,6 +694,20 @@ impl<'a> NodeFactory<'a> {
}))
}
/// Create a new [`Attribute`] from a attribute discrimination and a value
pub fn attr_disciption(
&self,
discription: AttributeDiscription,
val: Arguments,
) -> Attribute<'a> {
let (value, is_static) = self.raw_text(val);
Attribute {
attribute: discription,
is_static,
value: AttributeValue::Text(value),
}
}
/// Create a new [`Attribute`]
pub fn attr(
&self,
@@ -609,11 +718,13 @@ impl<'a> NodeFactory<'a> {
) -> Attribute<'a> {
let (value, is_static) = self.raw_text(val);
Attribute {
name,
value: AttributeValue::Text(value),
attribute: AttributeDiscription {
name,
namespace,
volatile: is_volatile,
},
is_static,
namespace,
is_volatile,
value: AttributeValue::Text(value),
}
}
@@ -627,11 +738,13 @@ impl<'a> NodeFactory<'a> {
is_static: bool,
) -> Attribute<'a> {
Attribute {
name,
value,
attribute: AttributeDiscription {
name,
namespace,
volatile: is_volatile,
},
is_static,
namespace,
is_volatile,
value,
}
}
@@ -743,6 +856,27 @@ impl<'a> NodeFactory<'a> {
let callback = RefCell::new(Some(caller));
EventHandler { callback }
}
/// Create a refrence to a template
pub fn template_ref(
&self,
id: TemplateId,
template: Template,
dynamic_context: TemplateContext<'a>,
) -> VNode<'a> {
let borrow_ref = self.scope.templates.borrow();
// We only create the template if it doesn't already exist to allow for hot reloading
if !borrow_ref.contains_key(&id) {
drop(borrow_ref);
let mut borrow_mut = self.scope.templates.borrow_mut();
borrow_mut.insert(id.clone(), Rc::new(RefCell::new(template)));
}
VNode::TemplateRef(self.bump.alloc(VTemplateRef {
id: empty_cell(),
dynamic_context,
template_id: id,
}))
}
}
impl Debug for NodeFactory<'_> {