diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index 6c6ecd2bdd..84fd92adc3 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -9,6 +9,9 @@ l0410 = { package = "leptos", version = "0.4.10", features = [ "ssr", ] } leptos = { path = "../leptos", features = ["ssr", "nightly"] } +leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] } +tachydom = { git = "https://github.com/gbj/tachys", features = ["nightly"] } +tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] } sycamore = { version = "0.8", features = ["ssr"] } yew = { version = "0.20", features = ["ssr"] } tokio-test = "0.4" diff --git a/benchmarks/src/ssr.rs b/benchmarks/src/ssr.rs index 7fe755569f..034a75cae9 100644 --- a/benchmarks/src/ssr.rs +++ b/benchmarks/src/ssr.rs @@ -36,6 +36,43 @@ fn leptos_ssr_bench(b: &mut Bencher) { r.dispose(); } +#[bench] +fn tachys_ssr_bench(b: &mut Bencher) { + use leptos::{create_runtime, create_signal, SignalGet, SignalUpdate}; + use tachy_maccy::view; + use tachydom::view::Render; + let rt = create_runtime(); + b.iter(|| { + fn counter(initial: i32) -> impl Render { + let (value, set_value) = create_signal(initial); + view! { +
+ + "Value: " {move || value().to_string()} "!" + +
+ } + } + + let mut buf = String::with_capacity(1024); + let rendered = view! { +
+

"Welcome to our benchmark page."

+

"Here's some introductory text."

+ {counter(1)} + {counter(2)} + {counter(3)} +
+ }; + rendered.to_html(&mut buf, &Default::default()); + assert_eq!( + buf, + "

Welcome to our benchmark page.

Here's some introductory text.

Value: 1!
Value: 2!
Value: 3!
" + ); + }); + rt.dispose(); +} + #[bench] fn tera_ssr_bench(b: &mut Bencher) { use serde::{Deserialize, Serialize}; diff --git a/benchmarks/src/todomvc/mod.rs b/benchmarks/src/todomvc/mod.rs index 428138159c..51df21d219 100644 --- a/benchmarks/src/todomvc/mod.rs +++ b/benchmarks/src/todomvc/mod.rs @@ -2,6 +2,7 @@ use test::Bencher; mod leptos; mod sycamore; +mod tachys; mod tera; mod yew; @@ -17,13 +18,32 @@ fn leptos_todomvc_ssr(b: &mut Bencher) { }); assert!(html.len() > 1); }); + runtime.dispose(); +} + +#[bench] +fn tachys_todomvc_ssr(b: &mut Bencher) { + use ::leptos::*; + let runtime = create_runtime(); + b.iter(|| { + use crate::todomvc::tachys::*; + use tachydom::view::Render; + + let mut buf = String::new(); + let rendered = TodoMVC(Todos::new()); + rendered.to_html(&mut buf, &Default::default()); + assert_eq!( + buf, + "

todos

    " + ); + }); + runtime.dispose(); } #[bench] fn sycamore_todomvc_ssr(b: &mut Bencher) { use self::sycamore::*; - use ::sycamore::prelude::*; - use ::sycamore::*; + use ::sycamore::{prelude::*, *}; b.iter(|| { _ = create_scope(|cx| { @@ -42,8 +62,7 @@ fn sycamore_todomvc_ssr(b: &mut Bencher) { #[bench] fn yew_todomvc_ssr(b: &mut Bencher) { use self::yew::*; - use ::yew::prelude::*; - use ::yew::ServerRenderer; + use ::yew::{prelude::*, ServerRenderer}; b.iter(|| { tokio_test::block_on(async { @@ -69,11 +88,26 @@ fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) { }); } +#[bench] +fn tachys_todomvc_ssr_with_1000(b: &mut Bencher) { + use ::leptos::*; + let runtime = create_runtime(); + b.iter(|| { + use crate::todomvc::tachys::*; + use tachydom::view::Render; + + let mut buf = String::new(); + let rendered = TodoMVC(Todos::new_with_1000()); + rendered.to_html(&mut buf, &Default::default()); + assert!(buf.len() > 20_000) + }); + runtime.dispose(); +} + #[bench] fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) { use self::sycamore::*; - use ::sycamore::prelude::*; - use ::sycamore::*; + use ::sycamore::{prelude::*, *}; b.iter(|| { _ = create_scope(|cx| { @@ -92,8 +126,7 @@ fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) { #[bench] fn yew_todomvc_ssr_with_1000(b: &mut Bencher) { use self::yew::*; - use ::yew::prelude::*; - use ::yew::ServerRenderer; + use ::yew::{prelude::*, ServerRenderer}; b.iter(|| { tokio_test::block_on(async { @@ -102,4 +135,19 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) { assert!(rendered.len() > 1); }); }); -} \ No newline at end of file +} + +#[bench] +fn tera_todomvc_ssr(b: &mut Bencher) { + use ::leptos::*; + let runtime = create_runtime(); + b.iter(|| { + use crate::todomvc::leptos::*; + + let html = ::leptos::ssr::render_to_string(|| { + view! { } + }); + assert!(html.len() > 1); + }); + runtime.dispose(); +} diff --git a/benchmarks/src/todomvc/tachys.rs b/benchmarks/src/todomvc/tachys.rs new file mode 100644 index 0000000000..dfd9891aae --- /dev/null +++ b/benchmarks/src/todomvc/tachys.rs @@ -0,0 +1,324 @@ +pub use leptos_reactive::*; +use miniserde::*; +use tachy_maccy::view; +use tachydom::view::Render; +use wasm_bindgen::JsCast; +use web_sys::HtmlInputElement; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Todos(pub Vec); + +const STORAGE_KEY: &str = "todos-leptos"; + +impl Todos { + pub fn new() -> Self { + Self(vec![]) + } + + pub fn new_with_1000() -> Self { + let todos = (0..1000) + .map(|id| Todo::new(id, format!("Todo #{id}"))) + .collect(); + Self(todos) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn add(&mut self, todo: Todo) { + self.0.push(todo); + } + + pub fn remove(&mut self, id: usize) { + self.0.retain(|todo| todo.id != id); + } + + pub fn remaining(&self) -> usize { + self.0.iter().filter(|todo| !(todo.completed)()).count() + } + + pub fn completed(&self) -> usize { + self.0.iter().filter(|todo| (todo.completed)()).count() + } + + pub fn toggle_all(&self) { + // if all are complete, mark them all active instead + if self.remaining() == 0 { + for todo in &self.0 { + if todo.completed.get() { + (todo.set_completed)(false); + } + } + } + // otherwise, mark them all complete + else { + for todo in &self.0 { + (todo.set_completed)(true); + } + } + } + + fn clear_completed(&mut self) { + self.0.retain(|todo| !todo.completed.get()); + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Todo { + pub id: usize, + pub title: ReadSignal, + pub set_title: WriteSignal, + pub completed: ReadSignal, + pub set_completed: WriteSignal, +} + +impl Todo { + pub fn new(id: usize, title: String) -> Self { + Self::new_with_completed(id, title, false) + } + + pub fn new_with_completed( + id: usize, + title: String, + completed: bool, + ) -> Self { + let (title, set_title) = create_signal(title); + let (completed, set_completed) = create_signal(completed); + Self { + id, + title, + set_title, + completed, + set_completed, + } + } + + pub fn toggle(&self) { + self.set_completed + .update(|completed| *completed = !*completed); + } +} + +const ESCAPE_KEY: u32 = 27; +const ENTER_KEY: u32 = 13; + +pub fn TodoMVC(todos: Todos) -> impl Render { + let mut next_id = todos + .0 + .iter() + .map(|todo| todo.id) + .max() + .map(|last| last + 1) + .unwrap_or(0); + + let (todos, set_todos) = create_signal(todos); + provide_context(set_todos); + + let (mode, set_mode) = create_signal(Mode::All); + + let add_todo = move |ev: web_sys::KeyboardEvent| { + todo!() + /* let target = event_target::(&ev); + ev.stop_propagation(); + let key_code = ev.unchecked_ref::().key_code(); + if key_code == ENTER_KEY { + let title = event_target_value(&ev); + let title = title.trim(); + if !title.is_empty() { + let new = Todo::new(next_id, title.to_string()); + set_todos.update(|t| t.add(new)); + next_id += 1; + target.set_value(""); + } + } */ + }; + + let filtered_todos = create_memo::>(move |_| { + todos.with(|todos| match mode.get() { + Mode::All => todos.0.to_vec(), + Mode::Active => todos + .0 + .iter() + .filter(|todo| !todo.completed.get()) + .cloned() + .collect(), + Mode::Completed => todos + .0 + .iter() + .filter(|todo| todo.completed.get()) + .cloned() + .collect(), + }) + }); + + // effect to serialize to JSON + // this does reactive reads, so it will automatically serialize on any relevant change + create_effect(move |_| { + () + /* if let Ok(Some(storage)) = window().local_storage() { + let objs = todos + .get() + .0 + .iter() + .map(TodoSerialized::from) + .collect::>(); + let json = json::to_string(&objs); + if storage.set_item(STORAGE_KEY, &json).is_err() { + log::error!("error while trying to set item in localStorage"); + } + } */ + }); + + view! { +
    +
    +
    +

    "todos"

    + +
    +
    + 0) + on:input=move |_| set_todos.update(|t| t.toggle_all()) + /> + +
      + {filtered_todos.get().into_iter().map(Todo).collect::>()} +
    +
    +
    + + {move || todos.with(|t| t.remaining().to_string())} + {move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }} + " left" + + + +
    +
    + +
    + } +} + +pub fn Todo(todo: Todo) -> impl Render { + let (editing, set_editing) = create_signal(false); + let set_todos = use_context::>().unwrap(); + //let input = NodeRef::new(); + + let save = move |value: &str| { + let value = value.trim(); + if value.is_empty() { + set_todos.update(|t| t.remove(todo.id)); + } else { + (todo.set_title)(value.to_string()); + } + set_editing(false); + }; + + view! { +
  • + /*
    + + + +
    + {move || { + editing() + .then(|| { + view! { + + } + }) + }} */ +
  • + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + Active, + Completed, + All, +} + +impl Default for Mode { + fn default() -> Self { + Mode::All + } +} + +pub fn route(hash: &str) -> Mode { + match hash { + "/active" => Mode::Active, + "/completed" => Mode::Completed, + _ => Mode::All, + } +} + +#[derive(Serialize, Deserialize)] +pub struct TodoSerialized { + pub id: usize, + pub title: String, + pub completed: bool, +} + +impl TodoSerialized { + pub fn into_todo(self) -> Todo { + Todo::new_with_completed(self.id, self.title, self.completed) + } +} + +impl From<&Todo> for TodoSerialized { + fn from(todo: &Todo) -> Self { + Self { + id: todo.id, + title: todo.title.get(), + completed: (todo.completed)(), + } + } +} diff --git a/benchmarks/src/todomvc/tera.rs b/benchmarks/src/todomvc/tera.rs index dd6946e096..73ecebe813 100644 --- a/benchmarks/src/todomvc/tera.rs +++ b/benchmarks/src/todomvc/tera.rs @@ -87,7 +87,7 @@ static TEMPLATE: &str = r#"
    "#; #[bench] -fn tera_todomvc(b: &mut Bencher) { +fn tera_todomvc_ssr(b: &mut Bencher) { use serde::{Deserialize, Serialize}; use tera::*; @@ -127,7 +127,7 @@ fn tera_todomvc(b: &mut Bencher) { } #[bench] -fn tera_todomvc_1000(b: &mut Bencher) { +fn tera_todomvc_ssr_1000(b: &mut Bencher) { use serde::{Deserialize, Serialize}; use tera::*; @@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) { let _ = TERA.render("template.html", &ctx).unwrap(); }); -} \ No newline at end of file +}