diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml
index f0ec24757..bebc5001e 100644
--- a/packages/router/Cargo.toml
+++ b/packages/router/Cargo.toml
@@ -25,10 +25,12 @@ js-sys = { version = "0.3.63", optional = true }
gloo-utils = { version = "0.1.6", optional = true }
dioxus-ssr = { path = "../ssr", optional = true }
lru = { version = "0.10.0", optional = true }
+radix_trie = { version = "0.2.1", optional = true }
+rustc-hash = "1.1.0"
[features]
default = ["web", "ssr"]
-ssr = ["dioxus-ssr", "lru"]
+ssr = ["dioxus-ssr", "lru", "radix_trie"]
wasm_test = []
serde = ["dep:serde", "gloo-utils/serde"]
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
@@ -36,6 +38,11 @@ web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
[dev-dependencies]
dioxus = { path = "../dioxus" }
dioxus-ssr = { path = "../ssr" }
+criterion = "0.3"
+
+[[bench]]
+name = "incremental"
+harness = false
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
dioxus-desktop = { path = "../desktop" }
diff --git a/packages/router/benches/incremental.rs b/packages/router/benches/incremental.rs
new file mode 100644
index 000000000..eba28b91a
--- /dev/null
+++ b/packages/router/benches/incremental.rs
@@ -0,0 +1,187 @@
+#![allow(unused, non_snake_case)]
+
+use std::time::Duration;
+
+use dioxus::prelude::*;
+use dioxus_router::prelude::*;
+
+use criterion::{black_box, criterion_group, criterion_main, Criterion};
+use dioxus_router::ssr::{DefaultRenderer, IncrementalRenderer};
+use dioxus_ssr::Renderer;
+
+pub fn criterion_benchmark(c: &mut Criterion) {
+ c.bench_function("build 1000 routes", |b| {
+ let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
+ before_body: r#"
+
+
+
+
+ Dioxus Application
+
+ "#
+ .to_string(),
+ after_body: r#"
+ "#
+ .to_string(),
+ })
+ .static_dir("./static")
+ .invalidate_after(Duration::from_secs(10))
+ .build();
+
+ b.iter(|| {
+ for id in 0..100 {
+ for id in 0..10 {
+ renderer
+ .render(Route::Post { id }, &mut std::io::sink())
+ .unwrap();
+ }
+ }
+ })
+ });
+ c.bench_function("build 1000 routes no memory cache", |b| {
+ let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
+ before_body: r#"
+
+
+
+
+ Dioxus Application
+
+ "#
+ .to_string(),
+ after_body: r#"
+ "#
+ .to_string(),
+ })
+ .static_dir("./static")
+ .memory_cache_limit(0)
+ .invalidate_after(Duration::from_secs(10))
+ .build();
+
+ b.iter(|| {
+ for id in 0..1000 {
+ renderer
+ .render(Route::Post { id }, &mut std::io::sink())
+ .unwrap();
+ }
+ })
+ });
+ c.bench_function("build 1000 routes no cache", |b| {
+ let mut renderer = Renderer::default();
+
+ b.iter(|| {
+ for id in 0..1000 {
+ let mut vdom = VirtualDom::new_with_props(
+ RenderPath,
+ RenderPathProps::builder().path(Route::Post { id }).build(),
+ );
+
+ vdom.rebuild();
+
+ struct Ignore;
+
+ impl std::fmt::Write for Ignore {
+ fn write_str(&mut self, s: &str) -> std::fmt::Result {
+ Ok(())
+ }
+ }
+
+ renderer.render_to(&mut Ignore, &vdom).unwrap();
+ }
+ })
+ });
+ c.bench_function("cache static", |b| {
+ b.iter(|| {
+ let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
+ before_body: r#"
+
+
+
+
+ Dioxus Application
+
+ "#
+ .to_string(),
+ after_body: r#"
+ "#
+ .to_string(),
+ })
+ .static_dir("./static")
+ .build();
+
+ renderer.pre_cache_static_routes::().unwrap();
+ })
+ });
+}
+
+criterion_group!(benches, criterion_benchmark);
+criterion_main!(benches);
+
+#[inline_props]
+fn Blog(cx: Scope) -> Element {
+ render! {
+ div {
+ "Blog"
+ }
+ }
+}
+
+#[inline_props]
+fn Post(cx: Scope, id: usize) -> Element {
+ render! {
+ for _ in 0..*id {
+ div {
+ "PostId: {id}"
+ }
+ }
+ }
+}
+
+#[inline_props]
+fn PostHome(cx: Scope) -> Element {
+ render! {
+ div {
+ "Post"
+ }
+ }
+}
+
+#[inline_props]
+fn Home(cx: Scope) -> Element {
+ render! {
+ div {
+ "Home"
+ }
+ }
+}
+
+#[rustfmt::skip]
+#[derive(Clone, Debug, PartialEq, Routable)]
+enum Route {
+ #[nest("/blog")]
+ #[route("/")]
+ Blog {},
+ #[route("/post/index")]
+ PostHome {},
+ #[route("/post/:id")]
+ Post {
+ id: usize,
+ },
+ #[end_nest]
+ #[route("/")]
+ Home {},
+}
+
+#[inline_props]
+fn RenderPath(cx: Scope, path: Route) -> Element {
+ let path = path.clone();
+ render! {
+ Router {
+ config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
+ }
+ }
+}
diff --git a/packages/router/examples/static_generation.rs b/packages/router/examples/static_generation.rs
index 00c08e0fa..0aa97cd80 100644
--- a/packages/router/examples/static_generation.rs
+++ b/packages/router/examples/static_generation.rs
@@ -1,5 +1,7 @@
#![allow(non_snake_case)]
+use std::time::Duration;
+
use dioxus::prelude::*;
use dioxus_router::prelude::*;
@@ -22,13 +24,16 @@ fn main() {
.to_string(),
})
.static_dir("./static")
+ .invalidate_after(Duration::from_secs(10))
.build();
- renderer.pre_cache_static::();
+ renderer.pre_cache_static_routes::().unwrap();
- for _ in 0..2 {
+ for _ in 0..1_000_000 {
for id in 0..10 {
- renderer.render(Route::Post { id });
+ renderer
+ .render(Route::Post { id }, &mut std::io::sink())
+ .unwrap();
}
}
}
diff --git a/packages/router/src/ssr.rs b/packages/router/src/ssr.rs
index f72f4ec6e..5aa8ef02f 100644
--- a/packages/router/src/ssr.rs
+++ b/packages/router/src/ssr.rs
@@ -4,17 +4,22 @@
use crate::prelude::*;
use dioxus::prelude::*;
+use rustc_hash::FxHasher;
use std::{
- io::{Read, Write},
+ hash::BuildHasherDefault,
+ io::Write,
num::NonZeroUsize,
path::{Path, PathBuf},
str::FromStr,
+ time::{Duration, SystemTime},
};
/// Something that can render a HTML page from a body.
pub trait RenderHTML {
- /// Render a HTML page from a body.
- fn render_html(&self, body: &str) -> String;
+ /// Render the HTML before the body
+ fn render_before_body(&self, to: &mut R) -> Result<(), IncrementalRendererError>;
+ /// Render the HTML after the body
+ fn render_after_body(&self, to: &mut R) -> Result<(), IncrementalRendererError>;
}
/// The default page renderer
@@ -45,8 +50,14 @@ impl Default for DefaultRenderer {
}
impl RenderHTML for DefaultRenderer {
- fn render_html(&self, body: &str) -> String {
- format!("{}{}{}", self.before_body, body, self.after_body)
+ fn render_before_body(&self, to: &mut R) -> Result<(), IncrementalRendererError> {
+ to.write_all(self.before_body.as_bytes())?;
+ Ok(())
+ }
+
+ fn render_after_body(&self, to: &mut R) -> Result<(), IncrementalRendererError> {
+ to.write_all(self.after_body.as_bytes())?;
+ Ok(())
}
}
@@ -54,6 +65,7 @@ impl RenderHTML for DefaultRenderer {
pub struct IncrementalRendererConfig {
static_dir: PathBuf,
memory_cache_limit: usize,
+ invalidate_after: Option,
render: R,
}
@@ -68,7 +80,8 @@ impl IncrementalRendererConfig {
pub fn new(render: R) -> Self {
Self {
static_dir: PathBuf::from("./static"),
- memory_cache_limit: 100,
+ memory_cache_limit: 10000,
+ invalidate_after: None,
render,
}
}
@@ -85,13 +98,21 @@ impl IncrementalRendererConfig {
self
}
+ /// Set the invalidation time.
+ pub fn invalidate_after(mut self, invalidate_after: Duration) -> Self {
+ self.invalidate_after = Some(invalidate_after);
+ self
+ }
+
/// Build the incremental renderer.
pub fn build(self) -> IncrementalRenderer {
IncrementalRenderer {
static_dir: self.static_dir,
memory_cache: NonZeroUsize::new(self.memory_cache_limit)
- .map(|limit| lru::LruCache::new(limit)),
+ .map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
+ invalidate_after: self.invalidate_after,
render: self.render,
+ ssr_renderer: dioxus_ssr::Renderer::new(),
}
}
}
@@ -99,7 +120,10 @@ impl IncrementalRendererConfig {
/// An incremental renderer.
pub struct IncrementalRenderer {
static_dir: PathBuf,
- memory_cache: Option>,
+ memory_cache:
+ Option), BuildHasherDefault>>,
+ invalidate_after: Option,
+ ssr_renderer: dioxus_ssr::Renderer,
render: R,
}
@@ -109,80 +133,157 @@ impl IncrementalRenderer {
IncrementalRendererConfig::new(renderer)
}
- fn render_uncached