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
+41
-2
@@ -7,7 +7,7 @@
|
||||
//! - tests to ensure dyn_into works for various event types.
|
||||
//! - Partial delegation?>
|
||||
|
||||
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
|
||||
use dioxus_core::{DomEdit, SchedulerMsg, UserEvent};
|
||||
use dioxus_html::event_bubbles;
|
||||
use dioxus_interpreter_js::Interpreter;
|
||||
use js_sys::Function;
|
||||
@@ -43,7 +43,7 @@ impl WebsysDom {
|
||||
break Ok(UserEvent {
|
||||
name: event_name_from_typ(&typ),
|
||||
data: virtual_event_from_websys_event(event.clone(), target.clone()),
|
||||
element: Some(ElementId(id)),
|
||||
element: Some(id),
|
||||
scope_id: None,
|
||||
priority: dioxus_core::EventPriority::Medium,
|
||||
bubbles: event.bubbles(),
|
||||
@@ -157,6 +157,45 @@ impl WebsysDom {
|
||||
let value = serde_wasm_bindgen::to_value(&value).unwrap();
|
||||
self.interpreter.SetAttribute(root, field, value, ns)
|
||||
}
|
||||
DomEdit::CreateTemplateRef { id, template_id } => {
|
||||
self.interpreter.CreateTemplateRef(id, template_id)
|
||||
}
|
||||
DomEdit::CreateTemplate { id } => self.interpreter.CreateTemplate(id),
|
||||
DomEdit::FinishTemplate { len } => self.interpreter.FinishTemplate(len),
|
||||
DomEdit::EnterTemplateRef { root } => self.interpreter.EnterTemplateRef(root),
|
||||
DomEdit::ExitTemplateRef {} => self.interpreter.ExitTemplateRef(),
|
||||
DomEdit::CreateTextNodeTemplate {
|
||||
root,
|
||||
text,
|
||||
locally_static,
|
||||
} => self
|
||||
.interpreter
|
||||
.CreateTextNodeTemplate(text, root, locally_static),
|
||||
DomEdit::CreateElementTemplate {
|
||||
root,
|
||||
tag,
|
||||
locally_static,
|
||||
fully_static,
|
||||
} => {
|
||||
self.interpreter
|
||||
.CreateElementTemplate(tag, root, locally_static, fully_static)
|
||||
}
|
||||
DomEdit::CreateElementNsTemplate {
|
||||
root,
|
||||
tag,
|
||||
ns,
|
||||
locally_static,
|
||||
fully_static,
|
||||
} => self.interpreter.CreateElementNsTemplate(
|
||||
tag,
|
||||
root,
|
||||
ns,
|
||||
locally_static,
|
||||
fully_static,
|
||||
),
|
||||
DomEdit::CreatePlaceholderTemplate { root } => {
|
||||
self.interpreter.CreatePlaceholderTemplate(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use dioxus_core::SchedulerMsg;
|
||||
use dioxus_core::SetTemplateMsg;
|
||||
use dioxus_core::VirtualDom;
|
||||
use dioxus_rsx_interpreter::error::Error;
|
||||
use dioxus_rsx_interpreter::{ErrorHandler, SetManyRsxMessage, RSX_CONTEXT};
|
||||
use futures_channel::mpsc::unbounded;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use futures_util::StreamExt;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{console, MessageEvent, WebSocket};
|
||||
use web_sys::{MessageEvent, WebSocket};
|
||||
|
||||
pub(crate) fn init(dom: &VirtualDom) {
|
||||
let window = web_sys::window().unwrap();
|
||||
@@ -23,46 +20,18 @@ pub(crate) fn init(dom: &VirtualDom) {
|
||||
);
|
||||
|
||||
let ws = WebSocket::new(&url).unwrap();
|
||||
let mut channel = dom.get_scheduler_channel();
|
||||
|
||||
// change the rsx when new data is received
|
||||
let cl = Closure::wrap(Box::new(|e: MessageEvent| {
|
||||
let cl = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||
if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
|
||||
let msgs: SetManyRsxMessage = serde_json::from_str(&format!("{text}")).unwrap();
|
||||
RSX_CONTEXT.extend(msgs);
|
||||
let msg: SetTemplateMsg = serde_json::from_str(&format!("{text}")).unwrap();
|
||||
channel
|
||||
.start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
|
||||
.unwrap();
|
||||
}
|
||||
}) as Box<dyn FnMut(MessageEvent)>);
|
||||
|
||||
ws.set_onmessage(Some(cl.as_ref().unchecked_ref()));
|
||||
cl.forget();
|
||||
|
||||
let (error_channel_sender, mut error_channel_receiver) = unbounded();
|
||||
|
||||
struct WebErrorHandler {
|
||||
sender: UnboundedSender<Error>,
|
||||
}
|
||||
|
||||
impl ErrorHandler for WebErrorHandler {
|
||||
fn handle_error(&self, err: dioxus_rsx_interpreter::error::Error) {
|
||||
self.sender.unbounded_send(err).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
RSX_CONTEXT.set_error_handler(WebErrorHandler {
|
||||
sender: error_channel_sender,
|
||||
});
|
||||
|
||||
RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel());
|
||||
|
||||
// forward stream to the websocket
|
||||
dom.base_scope().spawn_forever(async move {
|
||||
while let Some(err) = error_channel_receiver.next().await {
|
||||
if ws.ready_state() == WebSocket::OPEN {
|
||||
ws.send_with_str(serde_json::to_string(&err).unwrap().as_str())
|
||||
.unwrap();
|
||||
} else {
|
||||
console::warn_1(&"WebSocket is not open, cannot send error. Run with dioxus serve --hot-reload to enable hot reloading.".into());
|
||||
panic!("{}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -61,15 +61,14 @@ pub use crate::util::use_eval;
|
||||
use dioxus_core::prelude::Component;
|
||||
use dioxus_core::SchedulerMsg;
|
||||
use dioxus_core::VirtualDom;
|
||||
use futures_util::FutureExt;
|
||||
|
||||
mod cache;
|
||||
mod cfg;
|
||||
mod dom;
|
||||
#[cfg(feature = "hot-reload")]
|
||||
#[cfg(any(feature = "hot-reload", debug_assertions))]
|
||||
mod hot_reload;
|
||||
mod rehydrate;
|
||||
mod ric_raf;
|
||||
// mod ric_raf;
|
||||
mod util;
|
||||
|
||||
/// Launch the VirtualDOM given a root component and a configuration.
|
||||
@@ -169,7 +168,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot-reload")]
|
||||
#[cfg(any(feature = "hot-reload", debug_assertions))]
|
||||
hot_reload::init(&dom);
|
||||
|
||||
for s in crate::cache::BUILTIN_INTERNED_STRINGS {
|
||||
@@ -214,7 +213,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
|
||||
websys_dom.apply_edits(edits.edits);
|
||||
}
|
||||
|
||||
let mut work_loop = ric_raf::RafLoop::new();
|
||||
// let mut work_loop = ric_raf::RafLoop::new();
|
||||
|
||||
loop {
|
||||
log::trace!("waiting for work");
|
||||
@@ -225,13 +224,14 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
|
||||
log::trace!("working..");
|
||||
|
||||
// wait for the mainthread to schedule us in
|
||||
let mut deadline = work_loop.wait_for_idle_time().await;
|
||||
// let mut deadline = work_loop.wait_for_idle_time().await;
|
||||
|
||||
// run the virtualdom work phase until the frame deadline is reached
|
||||
let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());
|
||||
// let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());
|
||||
let mutations = dom.work_with_deadline(|| false);
|
||||
|
||||
// wait for the animation frame to fire so we can apply our changes
|
||||
work_loop.wait_for_raf().await;
|
||||
// work_loop.wait_for_raf().await;
|
||||
|
||||
for edit in mutations {
|
||||
// actually apply our changes during the animation frame
|
||||
|
||||
@@ -108,12 +108,30 @@ impl WebsysDom {
|
||||
}
|
||||
|
||||
for listener in vel.listeners {
|
||||
self.interpreter.NewEventListener(
|
||||
listener.event,
|
||||
listener.mounted_node.get().unwrap().as_u64(),
|
||||
self.handler.as_ref().unchecked_ref(),
|
||||
event_bubbles(listener.event),
|
||||
);
|
||||
let global_id = listener.mounted_node.get().unwrap();
|
||||
match global_id {
|
||||
dioxus_core::GlobalNodeId::TemplateId {
|
||||
template_ref_id,
|
||||
template_node_id: id,
|
||||
} => {
|
||||
self.interpreter.EnterTemplateRef(template_ref_id.into());
|
||||
self.interpreter.NewEventListener(
|
||||
listener.event,
|
||||
id.into(),
|
||||
self.handler.as_ref().unchecked_ref(),
|
||||
event_bubbles(listener.event),
|
||||
);
|
||||
self.interpreter.ExitTemplateRef();
|
||||
}
|
||||
dioxus_core::GlobalNodeId::VNodeId(id) => {
|
||||
self.interpreter.NewEventListener(
|
||||
listener.event,
|
||||
id.into(),
|
||||
self.handler.as_ref().unchecked_ref(),
|
||||
event_bubbles(listener.event),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !vel.listeners.is_empty() {
|
||||
@@ -165,6 +183,7 @@ impl WebsysDom {
|
||||
let node = scope.root_node();
|
||||
self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?;
|
||||
}
|
||||
VNode::TemplateRef(_) => todo!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user