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
+162
-28
@@ -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<'_> {
|
||||
|
||||
Reference in new issue
Block a user