perf: optimize inert HTML elements (#2989)

This commit is contained in:
Greg Johnston 2024-09-18 19:42:07 -04:00 committed by GitHub
parent ba9604101d
commit 5af7b54c9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 473 additions and 26 deletions

View file

@ -162,22 +162,24 @@ pub fn App() -> impl IntoView {
<table class="table table-hover table-striped test-data"> <table class="table table-hover table-striped test-data">
<tbody> <tbody>
<For <For
each={move || data.get()} each=move || data.get()
key={|row| row.id} key=|row| row.id
children=move |row: RowData| { children=move |row: RowData| {
let row_id = row.id; let row_id = row.id;
let label = row.label; let label = row.label;
let is_selected = is_selected.clone(); let is_selected = is_selected.clone();
ViewTemplate::new(view! { template! {
<tr class:danger={move || is_selected.selected(Some(row_id))}> < tr class : danger = { move || is_selected.selected(Some(row_id)) }
<td class="col-md-1">{row_id.to_string()}</td> > < td class = "col-md-1" > { row_id.to_string() } </ td > < td
<td class="col-md-4"><a on:click=move |_| set_selected.set(Some(row_id))>{move || label.get()}</a></td> class = "col-md-4" >< a on : click = move | _ | set_selected
<td class="col-md-1"><a on:click=move |_| remove(row_id)><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td> .set(Some(row_id)) > { move || label.get() } </ a ></ td > < td
<td class="col-md-6"/> class = "col-md-1" >< a on : click = move | _ | remove(row_id) ><
</tr> span class = "glyphicon glyphicon-remove" aria - hidden = "true" ></
}) span ></ a ></ td > < td class = "col-md-6" /> </ tr >
}
} }
/> />
</tbody> </tbody>
</table> </table>
<span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true"></span> <span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true"></span>

View file

@ -266,6 +266,21 @@ mod slot;
#[proc_macro] #[proc_macro]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn view(tokens: TokenStream) -> TokenStream { pub fn view(tokens: TokenStream) -> TokenStream {
view_macro_impl(tokens, false)
}
/// The `template` macro behaves like [`view`], except that it wraps the entire tree in a
/// [`ViewTemplate`](leptos::prelude::ViewTemplate). This optimizes creation speed by rendering
/// most of the view into a `<template>` tag with HTML rendered at compile time, then hydrating it.
/// In exchange, there is a small binary size overhead.
#[proc_macro_error2::proc_macro_error]
#[proc_macro]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn template(tokens: TokenStream) -> TokenStream {
view_macro_impl(tokens, true)
}
fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
let tokens: proc_macro2::TokenStream = tokens.into(); let tokens: proc_macro2::TokenStream = tokens.into();
let mut tokens = tokens.into_iter(); let mut tokens = tokens.into_iter();
@ -308,12 +323,13 @@ pub fn view(tokens: TokenStream) -> TokenStream {
&mut nodes, &mut nodes,
global_class.as_ref(), global_class.as_ref(),
normalized_call_site(proc_macro::Span::call_site()), normalized_call_site(proc_macro::Span::call_site()),
template,
); );
// The allow lint needs to be put here instead of at the expansion of // The allow lint needs to be put here instead of at the expansion of
// view::attribute_value(). Adding this next to the expanded expression // view::attribute_value(). Adding this next to the expanded expression
// seems to break rust-analyzer, but it works when the allow is put here. // seems to break rust-analyzer, but it works when the allow is put here.
quote! { let output = quote! {
{ {
#[allow(unused_braces)] #[allow(unused_braces)]
{ {
@ -321,6 +337,14 @@ pub fn view(tokens: TokenStream) -> TokenStream {
#nodes_output #nodes_output
} }
} }
};
if template {
quote! {
::leptos::prelude::ViewTemplate::new(#output)
}
} else {
output
} }
.into() .into()
} }

View file

@ -12,6 +12,7 @@ use syn::{spanned::Spanned, Expr, ExprPath, ExprRange, RangeLimits, Stmt};
pub(crate) fn component_to_tokens( pub(crate) fn component_to_tokens(
node: &mut NodeElement<impl CustomNode>, node: &mut NodeElement<impl CustomNode>,
global_class: Option<&TokenTree>, global_class: Option<&TokenTree>,
disable_inert_html: bool,
) -> TokenStream { ) -> TokenStream {
#[allow(unused)] // TODO this is used by hot-reloading #[allow(unused)] // TODO this is used by hot-reloading
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -191,6 +192,7 @@ pub(crate) fn component_to_tokens(
Some(&mut slots), Some(&mut slots),
global_class, global_class,
None, None,
disable_inert_html,
); );
// TODO view marker for hot-reloading // TODO view marker for hot-reloading

View file

@ -15,7 +15,7 @@ use rstml::node::{
}; };
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet, VecDeque},
}; };
use syn::{ use syn::{
spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprRange, Lit, LitStr, spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprRange, Lit, LitStr,
@ -34,6 +34,7 @@ pub fn render_view(
nodes: &mut [Node], nodes: &mut [Node],
global_class: Option<&TokenTree>, global_class: Option<&TokenTree>,
view_marker: Option<String>, view_marker: Option<String>,
disable_inert_html: bool,
) -> Option<TokenStream> { ) -> Option<TokenStream> {
let (base, should_add_view) = match nodes.len() { let (base, should_add_view) = match nodes.len() {
0 => { 0 => {
@ -52,6 +53,8 @@ pub fn render_view(
None, None,
global_class, global_class,
view_marker.as_deref(), view_marker.as_deref(),
true,
disable_inert_html,
), ),
// only add View wrapper and view marker to a regular HTML // only add View wrapper and view marker to a regular HTML
// element or component, not to a <{..} /> attribute list // element or component, not to a <{..} /> attribute list
@ -67,6 +70,7 @@ pub fn render_view(
None, None,
global_class, global_class,
view_marker.as_deref(), view_marker.as_deref(),
disable_inert_html,
), ),
true, true,
), ),
@ -91,12 +95,281 @@ pub fn render_view(
}) })
} }
fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
// do not use this if the top-level node is not an Element,
// or if it's an element with no children and no attrs
match orig_node {
Node::Element(el) => {
if el.attributes().is_empty() && el.children.is_empty() {
return false;
}
}
_ => return false,
}
// otherwise, walk over all the nodes to make sure everything is inert
let mut nodes = VecDeque::from([orig_node]);
while let Some(current_element) = nodes.pop_front() {
match current_element {
Node::Text(_) | Node::RawText(_) => {}
Node::Element(node) => {
if is_component_node(node) {
return false;
}
if is_spread_marker(node) {
return false;
}
match node.name() {
NodeName::Block(_) => return false,
_ => {
// check all attributes
for attr in node.attributes() {
match attr {
NodeAttribute::Block(_) => return false,
NodeAttribute::Attribute(attr) => {
let static_key =
!matches!(attr.key, NodeName::Block(_));
let static_value = match attr
.possible_value
.to_value()
{
None => true,
Some(value) => {
matches!(&value.value, KVAttributeValue::Expr(expr) if {
if let Expr::Lit(lit) = expr {
matches!(&lit.lit, Lit::Str(_))
} else {
false
}
})
}
};
if !static_key || !static_value {
return false;
}
}
}
}
// check all children
nodes.extend(&node.children);
}
}
}
_ => return false,
}
}
true
}
enum Item<'a, T> {
Node(&'a Node<T>),
ClosingTag(String),
}
enum InertElementBuilder<'a> {
GlobalClass {
global_class: &'a TokenTree,
strs: Vec<GlobalClassItem<'a>>,
buffer: String,
},
NoGlobalClass {
buffer: String,
},
}
impl<'a> ToTokens for InertElementBuilder<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
InertElementBuilder::GlobalClass { strs, .. } => {
tokens.extend(quote! {
[#(#strs),*].join("")
});
}
InertElementBuilder::NoGlobalClass { buffer } => {
tokens.extend(quote! {
#buffer
})
}
}
}
}
enum GlobalClassItem<'a> {
Global(&'a TokenTree),
String(String),
}
impl<'a> ToTokens for GlobalClassItem<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let addl_tokens = match self {
GlobalClassItem::Global(v) => v.to_token_stream(),
GlobalClassItem::String(v) => v.to_token_stream(),
};
tokens.extend(addl_tokens);
}
}
impl<'a> InertElementBuilder<'a> {
fn new(global_class: Option<&'a TokenTree>) -> Self {
match global_class {
None => Self::NoGlobalClass {
buffer: String::new(),
},
Some(global_class) => Self::GlobalClass {
global_class,
strs: Vec::new(),
buffer: String::new(),
},
}
}
fn push(&mut self, c: char) {
match self {
InertElementBuilder::GlobalClass { buffer, .. } => buffer.push(c),
InertElementBuilder::NoGlobalClass { buffer } => buffer.push(c),
}
}
fn push_str(&mut self, s: &str) {
match self {
InertElementBuilder::GlobalClass { buffer, .. } => {
buffer.push_str(s)
}
InertElementBuilder::NoGlobalClass { buffer } => buffer.push_str(s),
}
}
fn push_class(&mut self, class: &str) {
match self {
InertElementBuilder::GlobalClass {
global_class,
strs,
buffer,
} => {
buffer.push_str(" class=\"");
strs.push(GlobalClassItem::String(std::mem::take(buffer)));
strs.push(GlobalClassItem::Global(global_class));
buffer.push(' ');
buffer.push_str(class);
buffer.push('"');
}
InertElementBuilder::NoGlobalClass { buffer } => {
buffer.push_str(" class=\"");
buffer.push_str(class);
buffer.push('"');
}
}
}
fn finish(&mut self) {
match self {
InertElementBuilder::GlobalClass { strs, buffer, .. } => {
strs.push(GlobalClassItem::String(std::mem::take(buffer)));
}
InertElementBuilder::NoGlobalClass { .. } => {}
}
}
}
fn inert_element_to_tokens(
node: &Node<impl CustomNode>,
global_class: Option<&TokenTree>,
) -> Option<TokenStream> {
let mut html = InertElementBuilder::new(global_class);
let mut nodes = VecDeque::from([Item::Node(node)]);
while let Some(current) = nodes.pop_front() {
match current {
Item::ClosingTag(tag) => {
// closing tag
html.push_str("</");
html.push_str(&tag);
html.push('>');
}
Item::Node(current) => {
match current {
Node::RawText(raw) => {
let text = raw.to_string_best();
html.push_str(&text);
}
Node::Text(text) => {
let text = text.value_string();
html.push_str(&text);
}
Node::Element(node) => {
let self_closing = is_self_closing(node);
let el_name = node.name().to_string();
// opening tag
html.push('<');
html.push_str(&el_name);
for attr in node.attributes() {
if let NodeAttribute::Attribute(attr) = attr {
let attr_name = attr.key.to_string();
if attr_name != "class" {
html.push(' ');
html.push_str(&attr_name);
}
if let Some(value) =
attr.possible_value.to_value()
{
if let KVAttributeValue::Expr(Expr::Lit(
lit,
)) = &value.value
{
if let Lit::Str(txt) = &lit.lit {
if attr_name == "class" {
html.push_class(&txt.value());
} else {
html.push_str("=\"");
html.push_str(&txt.value());
html.push('"');
}
}
}
};
}
}
html.push('>');
// render all children
if !self_closing {
nodes.push_front(Item::ClosingTag(el_name));
let children = node.children.iter().rev();
for child in children {
nodes.push_front(Item::Node(child));
}
}
}
_ => {}
}
}
}
}
html.finish();
Some(quote! {
::leptos::tachys::html::InertElement::new(#html)
})
}
fn element_children_to_tokens( fn element_children_to_tokens(
nodes: &mut [Node<impl CustomNode>], nodes: &mut [Node<impl CustomNode>],
parent_type: TagType, parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>, parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>, global_class: Option<&TokenTree>,
view_marker: Option<&str>, view_marker: Option<&str>,
disable_inert_html: bool,
) -> Option<TokenStream> { ) -> Option<TokenStream> {
let children = children_to_tokens( let children = children_to_tokens(
nodes, nodes,
@ -104,6 +377,8 @@ fn element_children_to_tokens(
parent_slots, parent_slots,
global_class, global_class,
view_marker, view_marker,
false,
disable_inert_html,
); );
if children.is_empty() { if children.is_empty() {
None None
@ -145,6 +420,7 @@ fn fragment_to_tokens(
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>, parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>, global_class: Option<&TokenTree>,
view_marker: Option<&str>, view_marker: Option<&str>,
disable_inert_html: bool,
) -> Option<TokenStream> { ) -> Option<TokenStream> {
let children = children_to_tokens( let children = children_to_tokens(
nodes, nodes,
@ -152,6 +428,8 @@ fn fragment_to_tokens(
parent_slots, parent_slots,
global_class, global_class,
view_marker, view_marker,
true,
disable_inert_html,
); );
if children.is_empty() { if children.is_empty() {
None None
@ -183,6 +461,8 @@ fn children_to_tokens(
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>, parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>, global_class: Option<&TokenTree>,
view_marker: Option<&str>, view_marker: Option<&str>,
top_level: bool,
disable_inert_html: bool,
) -> Vec<TokenStream> { ) -> Vec<TokenStream> {
if nodes.len() == 1 { if nodes.len() == 1 {
match node_to_tokens( match node_to_tokens(
@ -191,6 +471,8 @@ fn children_to_tokens(
parent_slots, parent_slots,
global_class, global_class,
view_marker, view_marker,
top_level,
disable_inert_html,
) { ) {
Some(tokens) => vec![tokens], Some(tokens) => vec![tokens],
None => vec![], None => vec![],
@ -206,6 +488,8 @@ fn children_to_tokens(
Some(&mut slots), Some(&mut slots),
global_class, global_class,
view_marker, view_marker,
top_level,
disable_inert_html,
) )
}) })
.collect(); .collect();
@ -227,7 +511,11 @@ fn node_to_tokens(
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>, parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>, global_class: Option<&TokenTree>,
view_marker: Option<&str>, view_marker: Option<&str>,
top_level: bool,
disable_inert_html: bool,
) -> Option<TokenStream> { ) -> Option<TokenStream> {
let is_inert = !disable_inert_html && is_inert_element(node);
match node { match node {
Node::Comment(_) => None, Node::Comment(_) => None,
Node::Doctype(node) => { Node::Doctype(node) => {
@ -240,6 +528,7 @@ fn node_to_tokens(
parent_slots, parent_slots,
global_class, global_class,
view_marker, view_marker,
disable_inert_html,
), ),
Node::Block(block) => Some(quote! { #block }), Node::Block(block) => Some(quote! { #block }),
Node::Text(text) => Some(text_to_tokens(&text.value)), Node::Text(text) => Some(text_to_tokens(&text.value)),
@ -248,13 +537,20 @@ fn node_to_tokens(
let text = syn::LitStr::new(&text, raw.span()); let text = syn::LitStr::new(&text, raw.span());
Some(text_to_tokens(&text)) Some(text_to_tokens(&text))
} }
Node::Element(node) => element_to_tokens( Node::Element(el_node) => {
node, if !top_level && is_inert {
inert_element_to_tokens(node, global_class)
} else {
element_to_tokens(
el_node,
parent_type, parent_type,
parent_slots, parent_slots,
global_class, global_class,
view_marker, view_marker,
), disable_inert_html,
)
}
}
Node::Custom(node) => Some(node.to_token_stream()), Node::Custom(node) => Some(node.to_token_stream()),
} }
} }
@ -278,6 +574,7 @@ pub(crate) fn element_to_tokens(
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>, parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>, global_class: Option<&TokenTree>,
view_marker: Option<&str>, view_marker: Option<&str>,
disable_inert_html: bool,
) -> Option<TokenStream> { ) -> Option<TokenStream> {
// attribute sorting: // attribute sorting:
// //
@ -347,10 +644,16 @@ pub(crate) fn element_to_tokens(
if is_component_node(node) { if is_component_node(node) {
if let Some(slot) = get_slot(node) { if let Some(slot) = get_slot(node) {
let slot = slot.clone(); let slot = slot.clone();
slot_to_tokens(node, &slot, parent_slots, global_class); slot_to_tokens(
node,
&slot,
parent_slots,
global_class,
disable_inert_html,
);
None None
} else { } else {
Some(component_to_tokens(node, global_class)) Some(component_to_tokens(node, global_class, disable_inert_html))
} }
} else if is_spread_marker(node) { } else if is_spread_marker(node) {
let mut attributes = Vec::new(); let mut attributes = Vec::new();
@ -467,6 +770,7 @@ pub(crate) fn element_to_tokens(
parent_slots, parent_slots,
global_class, global_class,
view_marker, view_marker,
disable_inert_html,
) )
} else { } else {
if !node.children.is_empty() { if !node.children.is_empty() {

View file

@ -11,6 +11,7 @@ pub(crate) fn slot_to_tokens(
slot: &KeyedAttribute, slot: &KeyedAttribute,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>, parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>, global_class: Option<&TokenTree>,
disable_inert_html: bool,
) { ) {
let name = slot.key.to_string(); let name = slot.key.to_string();
let name = name.trim(); let name = name.trim();
@ -118,6 +119,7 @@ pub(crate) fn slot_to_tokens(
Some(&mut slots), Some(&mut slots),
global_class, global_class,
None, None,
disable_inert_html,
); );
// TODO view markers for hot-reloading // TODO view markers for hot-reloading

View file

@ -1,9 +1,12 @@
use self::attribute::Attribute;
use crate::{ use crate::{
hydration::Cursor,
no_attrs, no_attrs,
renderer::Renderer, prelude::AddAnyAttr,
view::{Position, Render, RenderHtml}, renderer::{CastFrom, DomRenderer, Renderer},
view::{Position, PositionState, Render, RenderHtml},
}; };
use std::marker::PhantomData; use std::{borrow::Cow, marker::PhantomData};
/// Types for HTML attributes. /// Types for HTML attributes.
pub mod attribute; pub mod attribute;
@ -76,8 +79,99 @@ where
fn hydrate<const FROM_SERVER: bool>( fn hydrate<const FROM_SERVER: bool>(
self, self,
_cursor: &crate::hydration::Cursor<R>, _cursor: &Cursor<R>,
_position: &crate::view::PositionState, _position: &PositionState,
) -> Self::State { ) -> Self::State {
} }
} }
/// An element that contains no interactivity, and whose contents can be known at compile time.
pub struct InertElement {
html: Cow<'static, str>,
}
impl InertElement {
/// Creates a new inert element.
pub fn new(html: impl Into<Cow<'static, str>>) -> Self {
Self { html: html.into() }
}
}
impl<Rndr> Render<Rndr> for InertElement
where
Rndr: DomRenderer,
{
type State = Rndr::Element;
fn build(self) -> Self::State {
Rndr::create_element_from_html(&self.html)
}
fn rebuild(self, _state: &mut Self::State) {}
}
impl<Rndr> AddAnyAttr<Rndr> for InertElement
where
Rndr: DomRenderer,
{
type Output<SomeNewAttr: Attribute<Rndr>> = Self;
fn add_any_attr<NewAttr: Attribute<Rndr>>(
self,
_attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Rndr>,
{
panic!(
"InertElement does not support adding attributes. It should only \
be used as a child, and not returned at the top level."
)
}
}
impl<Rndr> RenderHtml<Rndr> for InertElement
where
Rndr: DomRenderer,
{
type AsyncOutput = Self;
const MIN_LENGTH: usize = 0;
fn html_len(&self) -> usize {
self.html.len()
}
fn dry_resolve(&mut self) {}
async fn resolve(self) -> Self {
self
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut Position,
_escape: bool,
_mark_branches: bool,
) {
buf.push_str(&self.html);
*position = Position::NextChild;
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Rndr>,
position: &PositionState,
) -> Self::State {
let curr_position = position.get();
if curr_position == Position::FirstChild {
cursor.child();
} else if curr_position != Position::Current {
cursor.sibling();
}
let el = Rndr::Element::cast_from(cursor.current()).unwrap();
position.set(Position::NextChild);
el
}
}

View file

@ -390,6 +390,13 @@ impl DomRenderer for Dom {
.unwrap() .unwrap()
.unchecked_into() .unchecked_into()
} }
fn create_element_from_html(html: &str) -> Self::Element {
// TODO can be optimized to cache HTML strings or cache <template>?
let tpl = document().create_element("template").unwrap();
tpl.set_inner_html(html);
Self::clone_template(tpl.unchecked_ref())
}
} }
impl Mountable<Dom> for Node { impl Mountable<Dom> for Node {

View file

@ -300,6 +300,10 @@ impl DomRenderer for MockDom {
fn clone_template(tpl: &Self::TemplateElement) -> Self::Element { fn clone_template(tpl: &Self::TemplateElement) -> Self::Element {
todo!() todo!()
} }
fn create_element_from_html(html: &str) -> Self::Element {
todo!()
}
} }
impl Default for Document { impl Default for Document {

View file

@ -213,8 +213,12 @@ pub trait DomRenderer: Renderer {
fn get_template<V>() -> Self::TemplateElement fn get_template<V>() -> Self::TemplateElement
where where
V: ToTemplate + 'static; V: ToTemplate + 'static;
/// Deeply clones a template. /// Deeply clones a template.
fn clone_template(tpl: &Self::TemplateElement) -> Self::Element; fn clone_template(tpl: &Self::TemplateElement) -> Self::Element;
/// Creates a single element from a string of HTML.
fn create_element_from_html(html: &str) -> Self::Element;
} }
/// Attempts to cast from one type to another. /// Attempts to cast from one type to another.

View file

@ -560,6 +560,10 @@ impl DomRenderer for Sledgehammer {
}); });
node node
} }
fn create_element_from_html(html: &str) -> Self::Element {
todo!()
}
} }
impl Mountable<Sledgehammer> for SNode { impl Mountable<Sledgehammer> for SNode {