perf(ssr): optimize request throughput by 32%

- Cache ammonia::Builder with LazyLock (was rebuilt per request)
- Enable tracing release_max_level_info to strip tracing overhead at compile time
- Remove TraceLayer and tower-http trace feature from production
- Increase DB pool size 10→20 (configurable via DB_POOL_SIZE)
- Increase SSR cache TTL 300s→3600s (configurable via SSR_CACHE_SECS)

Benchmark: 7,444 → 9,840 req/s, P99 latency 27.6ms → 11.1ms
This commit is contained in:
xfy 2026-06-12 09:36:53 +08:00
parent 8dbe564ca2
commit bd9e87128d
8 changed files with 33 additions and 12 deletions

View File

@ -19,3 +19,9 @@ WEBP_METHOD=2
# Maximum concurrent sessions per user (default: 5, minimum: 1) # Maximum concurrent sessions per user (default: 5, minimum: 1)
MAX_SESSIONS_PER_USER=5 MAX_SESSIONS_PER_USER=5
# Database connection pool size (default: 20)
DB_POOL_SIZE=20
# SSR page cache duration in seconds (default: 3600)
SSR_CACHE_SECS=3600

View File

@ -41,6 +41,8 @@ RATE_LIMIT_UPLOAD_PER_SEC=2
RATE_LIMIT_UPLOAD_BURST=15 RATE_LIMIT_UPLOAD_BURST=15
RATE_LIMIT_IMAGE_PER_SEC=10 RATE_LIMIT_IMAGE_PER_SEC=10
RATE_LIMIT_IMAGE_BURST=50 RATE_LIMIT_IMAGE_BURST=50
DB_POOL_SIZE=20 # database connection pool size
SSR_CACHE_SECS=3600 # incremental SSR cache TTL
``` ```
Run migrations before first dev server start: Run migrations before first dev server start:

1
Cargo.lock generated
View File

@ -4379,7 +4379,6 @@ dependencies = [
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
"url", "url",
] ]

View File

@ -15,9 +15,9 @@ chrono = { version = "0.4", features = ["serde"] }
regex = "1.12" regex = "1.12"
pulldown-cmark = "0.13" pulldown-cmark = "0.13"
dotenvy = { version = "0.15", optional = true } dotenvy = { version = "0.15", optional = true }
tracing = { version = "0.1", optional = true } tracing = { version = "0.1", optional = true, features = ["release_max_level_info"] }
tracing-subscriber = { version = "0.3", optional = true } tracing-subscriber = { version = "0.3", optional = true }
tower-http = { version = "0.6", features = ["trace"], optional = true } tower-http = { version = "0.6", optional = true }
rand = { version = "0.8", features = ["getrandom"] } rand = { version = "0.8", features = ["getrandom"] }
getrandom = { version = "0.2", features = ["js"] } getrandom = { version = "0.2", features = ["js"] }
http = "1" http = "1"

View File

@ -9,7 +9,7 @@ fn html_escape(s: &str) -> String {
} }
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub fn clean_comment_html(input: &str) -> String { static COMMENT_AMMONIA_BUILDER: std::sync::LazyLock<ammonia::Builder> = std::sync::LazyLock::new(|| {
let mut builder = ammonia::Builder::default(); let mut builder = ammonia::Builder::default();
builder builder
.rm_tags(["img", "details", "summary"]) .rm_tags(["img", "details", "summary"])
@ -25,8 +25,12 @@ pub fn clean_comment_html(input: &str) -> String {
.add_tag_attributes("a", &["class", "aria-hidden", "aria-label"]) .add_tag_attributes("a", &["class", "aria-hidden", "aria-label"])
.add_tag_attributes("span", &["class"]) .add_tag_attributes("span", &["class"])
.link_rel(Some("nofollow noopener")); .link_rel(Some("nofollow noopener"));
builder
});
builder.clean(input).to_string() #[cfg(feature = "server")]
pub fn clean_comment_html(input: &str) -> String {
COMMENT_AMMONIA_BUILDER.clean(input).to_string()
} }
#[cfg(feature = "server")] #[cfg(feature = "server")]

View File

@ -1,7 +1,7 @@
#![allow(clippy::unused_unit, deprecated, unused_imports)] #![allow(clippy::unused_unit, deprecated, unused_imports)]
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub fn clean_html(input: &str) -> String { static AMMONIA_BUILDER: std::sync::LazyLock<ammonia::Builder> = std::sync::LazyLock::new(|| {
let mut builder = ammonia::Builder::default(); let mut builder = ammonia::Builder::default();
builder builder
.add_generic_attributes(&[ .add_generic_attributes(&[
@ -24,8 +24,12 @@ pub fn clean_html(input: &str) -> String {
.add_tag_attributes("h4", &["id", "class"]) .add_tag_attributes("h4", &["id", "class"])
.add_tag_attributes("h5", &["id", "class"]) .add_tag_attributes("h5", &["id", "class"])
.add_tag_attributes("h6", &["id", "class"]); .add_tag_attributes("h6", &["id", "class"]);
builder
});
builder.clean(input).to_string() #[cfg(feature = "server")]
pub fn clean_html(input: &str) -> String {
AMMONIA_BUILDER.clean(input).to_string()
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -16,7 +16,10 @@ pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
let mgr = Manager::from_config(pg_cfg, NoTls, mgr_cfg); let mgr = Manager::from_config(pg_cfg, NoTls, mgr_cfg);
Pool::builder(mgr) Pool::builder(mgr)
.max_size(10) .max_size(std::env::var("DB_POOL_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(20))
.build() .build()
.expect("Failed to create database connection pool") .expect("Failed to create database connection pool")
}); });

View File

@ -34,7 +34,6 @@ fn main() {
dioxus::server::serve(|| async move { dioxus::server::serve(|| async move {
use dioxus::server::{axum, DioxusRouterExt, ServeConfig}; use dioxus::server::{axum, DioxusRouterExt, ServeConfig};
use tower_http::trace::TraceLayer;
tokio::spawn(async { tokio::spawn(async {
tasks::ip_purge::run_purge().await; tasks::ip_purge::run_purge().await;
@ -46,7 +45,12 @@ fn main() {
let config = ServeConfig::builder().incremental( let config = ServeConfig::builder().incremental(
dioxus::server::IncrementalRendererConfig::default() dioxus::server::IncrementalRendererConfig::default()
.invalidate_after(std::time::Duration::from_secs(300)), .invalidate_after(std::time::Duration::from_secs(
std::env::var("SSR_CACHE_SECS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3600),
)),
); );
let api_routes = axum::Router::new().route( let api_routes = axum::Router::new().route(
"/api/upload", "/api/upload",
@ -64,8 +68,7 @@ fn main() {
let router = api_routes let router = api_routes
.merge(static_routes) .merge(static_routes)
.merge(dioxus_app) .merge(dioxus_app);
.layer(TraceLayer::new_for_http());
Ok(router) Ok(router)
}); });