Performance Optimization
Performance Optimization
Guide to optimizing Reinhardt application performance.
Table of Contents
Routing Performance
matchit Radix Tree
Reinhardt uses matchit Radix Tree for O(m) route matching (m = path length).
// Fast matching even with 1000+ routes
let router = ServerRouter::new()
.function("/api/resources/{id}", Method::GET, handler)
.function("/api/resources/{id}/posts", Method::GET, handler)
.function("/api/posts/{post_id}/comments/{comment_id}", Method::GET, handler);Route Registration Best Practices
use reinhardt::ServerRouter;
// ✅ Use hierarchical routing
let api_router = ServerRouter::new()
.with_prefix("/api/v1")
.function("/users", Method::GET, list_users);
let router = ServerRouter::new()
.mount("/api/", api_router);
// ❌ Register all routes flat (inefficient)
let router = ServerRouter::new()
.function("/api/v1/users", Method::GET, list_users)
.function("/api/v1/posts", Method::GET, list_posts);Lazy Compilation
Routes are compiled on first access, so there's no startup overhead.
let router = ServerRouter::new()
.function("/api/users", Method::GET, users_handler);
// Routes are compiled on first access
// Can explicitly compile with register_all_routes()
router.register_all_routes();Database Query Optimization
Efficient Queries with Reinhardt's Query Builder
use reinhardt::query::prelude::{Query, Postgres, Expr, Func, Order};
// ✅ Query using indexes
let (limit, offset) = (page_size, (page - 1) * page_size);
let query = Query::select()
.columns([User::Id, User::Username, User::Email])
.from(User::Table)
.and_where(Expr::col(User::IsActive).eq(true))
.order_by(User::CreatedAt, Order::Desc) // Sort by indexed column
.limit(limit as u64)
.offset(offset as u64)
.to_owned();
// ✅ Count query is separate
let count_query = Query::select()
.expr(Func::count(Expr::col(User::Id)))
.from(User::Table)
.and_where(Expr::col(User::IsActive).eq(true))
.to_owned();Avoid N+1 Problem
use reinhardt_db::orm::{NPlusOneConfig, NPlusOneScope};
// ❌ N+1 problem
let posts = Post::objects().all().await?;
for post in posts {
let user = post.user().await?; // Query in each loop
}
// ✅ Use a JOIN for a single-object relationship
let posts = Post::objects()
.select_related(&["user"])
.all()
.await?;
// ✅ Use prefetching or a batch query for collections
let posts = Post::objects()
.prefetch_related(&["comments"])
.all()
.await?;
// ✅ Make repeated query shapes visible in focused diagnostics
let (_, report) = NPlusOneScope::warn("posts.index", NPlusOneConfig::default())
.run_with_report(async {
render_posts().await
})
.await;
for finding in report.findings {
tracing::warn!(
fingerprint = %finding.fingerprint,
count = finding.execution_count,
"Repeated query shape detected"
);
}Use NPlusOneScope::fail(...).run(...) in focused tests when a path must stay batched. QuerySet execution and relationship accessors are recorded by active scopes. Use NPlusOneScope::spawn(...) for spawned tasks that should inherit the active scope. The detector is disabled unless a scope is installed, so production requests are unchanged by default.
Cache Strategies
HTTP Caching
HTTP caching using CacheControlMiddleware.
use reinhardt::staticfiles::{CacheControlMiddleware, CacheControlConfig};
let config = CacheControlConfig {
public: true,
max_age: 3600, // 1 hour
s_maxage: Some(86400), // 1 day on CDN
immutable: true, // Versioned files
};
let router = ServerRouter::new()
.with_middleware(CacheControlMiddleware::new(config));Cache Directives
use reinhardt::staticfiles::CacheDirective;
// Versioned files (cache for 1 year)
let immutable = CacheDirective::public()
.with_max_age(31536000)
.with_immutable(true);
// API responses (cache for 5 minutes)
let api_cache = CacheDirective::public()
.with_max_age(300)
.with_private(true); // User-specific data
// HTML files (don't cache)
let no_cache = CacheDirective::no_cache();ETag Conditional Requests
use reinhardt::ETagMiddleware;
let router = ServerRouter::new()
.with_middleware(ETagMiddleware::new(ETagConfig::default()));
// When client sends If-None-Match header,
// returns 304 Not Modified if unchangedCompression Settings
GZip Compression
use reinhardt::{GZipMiddleware, GZipConfig};
let config = GZipConfig {
level: 6, // Compression level (1-9, default 6)
min_size: 1024, // Minimum file size to compress
};
let router = ServerRouter::new()
.with_middleware(GZipMiddleware::new(config));Brotli Compression
use reinhardt::{BrotliMiddleware, BrotliConfig};
let config = BrotliConfig {
quality: BrotliQuality::Medium, // Compression quality
min_size: 1024,
};
let router = ServerRouter::new()
.with_middleware(BrotliMiddleware::new(config));Choosing Compression Algorithm
| Format | Compression | Speed | Use Case |
|---|---|---|---|
| GZip | Medium | Fast | General text, JSON, HTML |
| Brotli | High | Medium | JavaScript, CSS, large static files |
Compression Best Practices
// ✅ Provide both Brotli and GZip (browser chooses)
let router = ServerRouter::new()
.with_middleware(BrotliMiddleware::new(brotli_config))
.with_middleware(GZipMiddleware::new(gzip_config));
// Compressed response:
// Content-Encoding: br, gzipProfiling
Measuring Request Time
use reinhardt::{MetricsMiddleware, MetricsConfig};
use std::time::Instant;
async fn timed_handler(req: Request) -> Response {
let start = Instant::now();
// Handler processing
let response = process_request(req).await;
let duration = start.elapsed();
println!("Request took {:?}", duration);
response
}Metrics Collection
use reinhardt::{MetricsMiddleware, MetricsStore};
let store = MetricsStore::new();
let middleware = MetricsMiddleware::new(MetricsConfig::default());
let router = ServerRouter::new()
.with_middleware(middleware);