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)
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_IMAGE_PER_SEC=10
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:

1
Cargo.lock generated
View File

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

View File

@ -15,9 +15,9 @@ chrono = { version = "0.4", features = ["serde"] }
regex = "1.12"
pulldown-cmark = "0.13"
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 }
tower-http = { version = "0.6", features = ["trace"], optional = true }
tower-http = { version = "0.6", optional = true }
rand = { version = "0.8", features = ["getrandom"] }
getrandom = { version = "0.2", features = ["js"] }
http = "1"

View File

@ -9,7 +9,7 @@ fn html_escape(s: &str) -> String {
}
#[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();
builder
.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("span", &["class"])
.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")]

View File

@ -1,7 +1,7 @@
#![allow(clippy::unused_unit, deprecated, unused_imports)]
#[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();
builder
.add_generic_attributes(&[
@ -24,8 +24,12 @@ pub fn clean_html(input: &str) -> String {
.add_tag_attributes("h4", &["id", "class"])
.add_tag_attributes("h5", &["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)]

View File

@ -16,7 +16,10 @@ pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
let mgr = Manager::from_config(pg_cfg, NoTls, mgr_cfg);
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()
.expect("Failed to create database connection pool")
});

View File

@ -34,7 +34,6 @@ fn main() {
dioxus::server::serve(|| async move {
use dioxus::server::{axum, DioxusRouterExt, ServeConfig};
use tower_http::trace::TraceLayer;
tokio::spawn(async {
tasks::ip_purge::run_purge().await;
@ -46,7 +45,12 @@ fn main() {
let config = ServeConfig::builder().incremental(
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(
"/api/upload",
@ -64,8 +68,7 @@ fn main() {
let router = api_routes
.merge(static_routes)
.merge(dioxus_app)
.layer(TraceLayer::new_for_http());
.merge(dioxus_app);
Ok(router)
});