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

+18 -11
View File
@@ -1,6 +1,6 @@
use crate::{node::PreventDefault, Dom};
use dioxus_core::ElementId;
use dioxus_core::GlobalNodeId;
use dioxus_native_core::utils::{ElementProduced, PersistantElementIter};
use dioxus_native_core_macro::sorted_str_slice;
@@ -69,7 +69,10 @@ impl NodeDepState<()> for Focus {
fn reduce(&mut self, node: NodeView<'_>, _sibling: (), _: &Self::Ctx) -> bool {
let new = Focus {
level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") {
level: if let Some(a) = node
.attributes()
.and_then(|mut a| a.find(|a| a.attribute.name == "tabindex"))
{
if let Some(index) = a
.value
.as_int32()
@@ -87,8 +90,12 @@ impl NodeDepState<()> for Focus {
}
} else if node
.listeners()
.iter()
.any(|l| FOCUS_EVENTS.binary_search(&l.event).is_ok())
.map(|mut listeners| {
listeners
.any(|l| FOCUS_EVENTS.binary_search(&l).is_ok())
.then(|| ())
})
.is_some()
{
FocusLevel::Focusable
} else {
@@ -110,7 +117,7 @@ const FOCUS_ATTRIBUTES: &[&str] = &sorted_str_slice!(["tabindex"]);
#[derive(Default)]
pub(crate) struct FocusState {
pub(crate) focus_iter: PersistantElementIter,
pub(crate) last_focused_id: Option<ElementId>,
pub(crate) last_focused_id: Option<GlobalNodeId>,
pub(crate) focus_level: FocusLevel,
pub(crate) dirty: bool,
}
@@ -224,9 +231,9 @@ impl FocusState {
pub(crate) fn prune(&mut self, mutations: &dioxus_core::Mutations, rdom: &Dom) {
fn remove_children(
to_prune: &mut [&mut Option<ElementId>],
to_prune: &mut [&mut Option<GlobalNodeId>],
rdom: &Dom,
removed: ElementId,
removed: GlobalNodeId,
) {
for opt in to_prune.iter_mut() {
if let Some(id) = opt {
@@ -235,7 +242,7 @@ impl FocusState {
}
}
}
if let NodeType::Element { children, .. } = &rdom[removed].node_type {
if let NodeType::Element { children, .. } = &rdom[removed].node_data.node_type {
for child in children {
remove_children(to_prune, rdom, *child);
}
@@ -249,19 +256,19 @@ impl FocusState {
dioxus_core::DomEdit::ReplaceWith { root, .. } => remove_children(
&mut [&mut self.last_focused_id],
rdom,
ElementId(*root as usize),
rdom.decode_id(*root),
),
dioxus_core::DomEdit::Remove { root } => remove_children(
&mut [&mut self.last_focused_id],
rdom,
ElementId(*root as usize),
rdom.decode_id(*root),
),
_ => (),
}
}
}
pub(crate) fn set_focus(&mut self, rdom: &mut Dom, id: ElementId) {
pub(crate) fn set_focus(&mut self, rdom: &mut Dom, id: GlobalNodeId) {
if let Some(old) = self.last_focused_id.replace(id) {
rdom[old].state.focused = false;
}
+9 -8
View File
@@ -243,23 +243,24 @@ impl InnerInputState {
fn try_create_event(
name: &'static str,
data: Arc<dyn Any + Send + Sync>,
will_bubble: &mut FxHashSet<ElementId>,
will_bubble: &mut FxHashSet<GlobalNodeId>,
resolved_events: &mut Vec<UserEvent>,
node: &Node,
dom: &Dom,
) {
// only trigger event if the event was not triggered already by a child
if will_bubble.insert(node.id) {
let mut parent = node.parent;
let id = node.node_data.id;
if will_bubble.insert(id) {
let mut parent = node.node_data.parent;
while let Some(parent_id) = parent {
will_bubble.insert(parent_id);
parent = dom[parent_id].parent;
parent = dom[parent_id].node_data.parent;
}
resolved_events.push(UserEvent {
scope_id: None,
priority: EventPriority::Medium,
name,
element: Some(node.id),
element: Some(id),
data,
bubbles: event_bubbles(name),
})
@@ -546,7 +547,7 @@ impl InnerInputState {
let currently_contains = layout_contains_point(node_layout, new_pos);
if currently_contains && node.state.focus.level.focusable() {
focus_id = Some(node.id);
focus_id = Some(node.node_data.id);
}
});
if let Some(id) = focus_id {
@@ -565,7 +566,7 @@ fn get_abs_layout(node: &Node, dom: &Dom, taffy: &Taffy) -> Layout {
let mut node_layout = *taffy.layout(node.state.layout.node.unwrap()).unwrap();
let mut current = node;
while let Some(parent_id) = current.parent {
while let Some(parent_id) = current.node_data.parent {
let parent = &dom[parent_id];
current = parent;
let parent_layout = taffy.layout(parent.state.layout.node.unwrap()).unwrap();
@@ -664,7 +665,7 @@ impl RinkInputHandler {
scope_id: None,
priority: EventPriority::Medium,
name: event,
element: Some(node.id),
element: Some(node.node_data.id),
data: data.clone(),
bubbles: event_bubbles(event),
});
+12 -4
View File
@@ -4,6 +4,7 @@ use std::rc::Rc;
use dioxus_core::*;
use dioxus_native_core::layout_attributes::apply_layout_attributes;
use dioxus_native_core::node_ref::{AttributeMask, NodeMask, NodeView};
use dioxus_native_core::real_dom::OwnedAttributeView;
use dioxus_native_core::state::ChildDepState;
use dioxus_native_core_macro::sorted_str_slice;
use taffy::prelude::*;
@@ -84,10 +85,17 @@ impl ChildDepState for TaffyLayout {
}
} else {
// gather up all the styles from the attribute list
for Attribute { name, value, .. } in node.attributes() {
assert!(SORTED_LAYOUT_ATTRS.binary_search(name).is_ok());
if let Some(text) = value.as_text() {
apply_layout_attributes(name, text, &mut style);
if let Some(attributes) = node.attributes() {
for OwnedAttributeView {
attribute, value, ..
} in attributes
{
assert!(SORTED_LAYOUT_ATTRS
.binary_search(&attribute.name.as_ref())
.is_ok());
if let Some(text) = value.as_text() {
apply_layout_attributes(&&attribute.name, text, &mut style);
}
}
}
+6 -4
View File
@@ -94,7 +94,7 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) {
let to_update = rdom.apply_mutations(vec![mutations]);
let mut any_map = AnyMap::new();
any_map.insert(taffy.clone());
let _to_rerender = rdom.update_state(&dom, to_update, any_map);
let _to_rerender = rdom.update_state(to_update, any_map);
}
render_vdom(
@@ -133,8 +133,10 @@ fn render_vdom(
terminal.clear().unwrap();
}
let mut to_rerender: fxhash::FxHashSet<ElementId> =
vec![ElementId(0)].into_iter().collect();
let mut to_rerender: fxhash::FxHashSet<GlobalNodeId> =
vec![GlobalNodeId::VNodeId(ElementId(0))]
.into_iter()
.collect();
let mut updated = true;
loop {
@@ -250,7 +252,7 @@ fn render_vdom(
// update the style and layout
let mut any_map = AnyMap::new();
any_map.insert(taffy.clone());
to_rerender = rdom.update_state(vdom, to_update, any_map);
to_rerender = rdom.update_state(to_update, any_map);
}
}
+5 -5
View File
@@ -61,11 +61,11 @@ impl NodeDepState<()> for PreventDefault {
_sibling: (),
_ctx: &Self::Ctx,
) -> bool {
let new = match node
.attributes()
.find(|a| a.name == "dioxus-prevent-default")
.and_then(|a| a.value.as_text())
{
let new = match node.attributes().and_then(|mut attrs| {
attrs
.find(|a| a.attribute.name == "dioxus-prevent-default")
.and_then(|a| a.value.as_text())
}) {
Some("onfocus") => PreventDefault::Focus,
Some("onkeypress") => PreventDefault::KeyPress,
Some("onkeyrelease") => PreventDefault::KeyRelease,
+2 -2
View File
@@ -25,7 +25,7 @@ pub(crate) fn render_vnode(
) {
use dioxus_native_core::real_dom::NodeType;
if let NodeType::Placeholder = &node.node_type {
if let NodeType::Placeholder = &node.node_data.node_type {
return;
}
@@ -38,7 +38,7 @@ pub(crate) fn render_vnode(
let Point { x, y } = location;
let Size { width, height } = size;
match &node.node_type {
match &node.node_data.node_type {
NodeType::Text { text } => {
#[derive(Default)]
struct Label<'a> {
+9 -4
View File
@@ -29,10 +29,10 @@
- [ ] pub aspect_ratio: Number,
*/
use dioxus_core::Attribute;
use dioxus_native_core::{
layout_attributes::parse_value,
node_ref::{AttributeMask, NodeMask, NodeView},
real_dom::OwnedAttributeView,
state::ParentDepState,
};
use dioxus_native_core_macro::sorted_str_slice;
@@ -79,9 +79,14 @@ impl ParentDepState for StyleModifier {
}
// gather up all the styles from the attribute list
for Attribute { name, value, .. } in node.attributes() {
if let Some(text) = value.as_text() {
apply_style_attributes(name, text, &mut new);
if let Some(attrs) = node.attributes() {
for OwnedAttributeView {
attribute, value, ..
} in attrs
{
if let Some(text) = value.as_text() {
apply_style_attributes(&attribute.name, text, &mut new);
}
}
}