Blazouter Caching System
December 23, 2025 · View on GitHub
Overview
Blazouter includes a sophisticated caching system to optimize route matching and component loading performance. The caching layer is transparent, requiring no code changes while significantly improving navigation speed, especially for applications with complex routing structures or lazy-loaded components.
Features
- Route Match Caching: Caches route matching results for faster subsequent navigations
- Component Type Caching: Lazily loaded components are cached to avoid repeated async loading
- LRU Eviction: Least Recently Used (LRU) policy ensures efficient memory usage for route cache
- FIFO Eviction: First In First Out (FIFO) policy for component type cache
- TTL Support: Optional time-to-live for cache entries
- Thread-Safe: Concurrent-safe implementation for server-side scenarios
- Statistics Tracking: Monitor cache performance with detailed metrics
Architecture
The caching system consists of four layers:
1. Models
CacheOptions
Configuration options for cache behavior:
public class CacheOptions
{
// Enable/disable route match caching
public bool EnableRouteMatchCache { get; set; } = true;
// Enable/disable component type caching
public bool EnableComponentTypeCache { get; set; } = true;
// Maximum number of route matches to cache (default: 100)
public int MaxRouteMatchCacheSize { get; set; } = 100;
// Maximum number of component types to cache (default: 50)
public int MaxComponentTypeCacheSize { get; set; } = 50;
// Time-to-live in seconds for route matches (0 = no expiration)
public int RouteMatchCacheTTLSeconds { get; set; } = 0;
// Enable statistics tracking
public bool EnableStatistics { get; set; } = false;
}
CacheStatistics
Performance metrics for cache operations:
public class CacheStatistics
{
public long TotalRequests { get; set; } // Total cache lookups
public long CacheHits { get; set; } // Successful cache hits
public long CacheMisses { get; set; } // Cache misses
public double HitRate { get; } // Hit rate percentage
public int RouteMatchCacheSize { get; set; } // Current route cache size
public int ComponentTypeCacheSize { get; set; } // Current component cache size
}
2. Interfaces
IRouteCacheService
Defines caching operations:
public interface IRouteCacheService
{
// Route match cache operations
RouteMatch? GetCachedRouteMatch(string path);
void CacheRouteMatch(string path, RouteMatch match);
void InvalidateRouteMatch(string path);
// Component type cache operations
Type? GetCachedComponentType(string routePath);
void CacheComponentType(string routePath, Type componentType);
// Management operations
void Clear();
CacheStatistics GetStatistics();
}
3. Services
RouteCacheService
Thread-safe implementation using:
ConcurrentDictionaryfor concurrent accessInterlockedoperations for atomic updates- LRU eviction for route matches
- FIFO eviction for component types
CachedRouteMatcherService
Decorator pattern implementation that wraps RouteMatcherService with caching:
public RouteMatch? MatchRoute(string path, List<RouteConfig> routes)
{
// 1. Check cache
var cachedMatch = _cacheService.GetCachedRouteMatch(path);
if (cachedMatch != null)
return cachedMatch;
// 2. Cache miss - perform actual matching
var match = _innerMatcher.MatchRoute(path, routes);
// 3. Cache successful matches
if (match != null && (match.Route?.EnableCache ?? true))
_cacheService.CacheRouteMatch(path, match);
return match;
}
4. Extensions
RouteCacheExtensions
Helper methods for working with cache:
public static string GetFormattedStatistics(this IRouteCacheService cacheService)
{
var stats = cacheService.GetStatistics();
return $@"Cache Statistics:
Total Requests: {stats.TotalRequests:N0}
Cache Hits: {stats.CacheHits:N0}
Cache Misses: {stats.CacheMisses:N0}
Hit Rate: {stats.HitRate:F2}%
Route Cache Size: {stats.RouteMatchCacheSize}
Component Cache Size: {stats.ComponentTypeCacheSize}";
}
How It Works
Route Match Caching
User Navigates to "/users/123"
↓
CachedRouteMatcherService.MatchRoute()
↓
Check Cache → GetCachedRouteMatch("/users/123")
↓
├─→ Cache Hit (0.1ms)
│ └─→ Return cached RouteMatch
│
└─→ Cache Miss (1-10ms)
├─→ Perform route matching
├─→ Store in cache
└─→ Return RouteMatch
Component Type Caching
Lazy Load Request
↓
Router checks cache → GetCachedComponentType(routePath)
↓
├─→ Cache Hit
│ └─→ Return cached Type (instant!)
│
└─→ Cache Miss
├─→ Execute ComponentLoader
├─→ Store Type in cache
└─→ Return Type
Configuration
Default Configuration
Caching is enabled by default with sensible defaults:
// In Program.cs
builder.Services.AddBlazouter();
Default settings:
- Route match cache: Enabled (100 entries)
- Component type cache: Enabled (50 entries)
- TTL: No expiration
- Statistics: Disabled
Custom Configuration
Fine-tune caching for your application:
builder.Services.AddBlazouter(options =>
{
// Increase cache sizes for large applications
options.MaxRouteMatchCacheSize = 200;
options.MaxComponentTypeCacheSize = 100;
// Enable statistics for monitoring
options.EnableStatistics = true;
// Set TTL for development (routes might change)
options.RouteMatchCacheTTLSeconds = 300; // 5 minutes
// Disable specific caches if needed
options.EnableRouteMatchCache = true;
options.EnableComponentTypeCache = true;
});
Per-Route Cache Control
Control caching at the route level:
new RouteConfig
{
Path = "/admin/dashboard",
Component = typeof(AdminDashboard),
EnableCache = false // Never cache this route
}
new RouteConfig
{
Path = "/static-page",
Component = typeof(StaticPage),
EnableCache = true // Always cache (even if global caching disabled)
}
new RouteConfig
{
Path = "/default",
Component = typeof(DefaultPage),
EnableCache = null // Use global settings (default)
}
Use cases for disabling cache per route:
- Admin dashboards with real-time data
- User-specific pages that change frequently
- Routes with middleware that should run every time
- Routes with dynamic content
Attribute-Based Cache Control
You can also control caching using the [RouteCache] attribute on your component class:
using Blazouter.Attributes;
using Microsoft.AspNetCore.Components;
using RouteAttribute = Blazouter.Attributes.RouteAttribute;
// Disable caching for this route
[Route("/admin/dashboard")]
[RouteCache(false)]
public class AdminDashboard : ComponentBase { }
// Force caching even if global caching is disabled
[Route("/static-page")]
[RouteCache(true)]
public class StaticPage : ComponentBase { }
// Use global settings (default - attribute not needed)
[Route("/default")]
public class DefaultPage : ComponentBase { }
Attribute values:
[RouteCache(false)]- Never cache this route[RouteCache(true)]- Always cache this route- No attribute - Use global cache settings (default)
Cache Statistics
Monitor cache performance:
@inject IRouteCacheService CacheService
@code {
private void ShowStats()
{
var stats = CacheService.GetStatistics();
Console.WriteLine($"Hit Rate: {stats.HitRate:F2}%");
Console.WriteLine($"Total Requests: {stats.TotalRequests}");
Console.WriteLine($"Route Cache Size: {stats.RouteMatchCacheSize}");
Console.WriteLine($"Component Cache Size: {stats.ComponentTypeCacheSize}");
}
}
Note: Statistics tracking must be enabled in cache options.
Cache Management
Programmatically manage cache entries:
@inject IRouteCacheService CacheService
@code {
// Clear all cached entries
private void ClearCache()
{
CacheService.Clear();
}
// Invalidate specific route
private void InvalidateRoute(string path)
{
CacheService.InvalidateRouteMatch(path);
}
}
Performance Characteristics
Cache Hit vs Cache Miss
Cache Hit:
- Navigation Time: ~0.1ms (dictionary lookup)
- Performance Gain: 10-100x faster than cache miss
Cache Miss:
- Navigation Time: ~1-10ms (route matching algorithm)
- Subsequent operations will benefit from cache
Memory Usage
Route Match Cache:
- Entry size: ~200-500 bytes
- Max memory (100 entries): ~20-50 KB
- Max memory (200 entries): ~40-100 KB
Component Type Cache:
- Entry size: ~100-200 bytes
- Max memory (50 entries): ~5-10 KB
- Max memory (100 entries): ~10-20 KB
Total: ~25-110 KB (default settings)
Thread Safety
- Singleton service shared across all users
- Thread-safe concurrent operations
- No locking required
- Safe for server-side scenarios
Cache Strategies
Route Match Cache - LRU (Least Recently Used)
Why LRU?
- Frequently accessed routes stay in cache
- Rarely used routes get evicted
- Controlled memory growth
How it works:
- Each cache hit updates
LastAccessedTicks - When cache is full, entry with lowest
LastAccessedTicksis removed - Thread-safe using
Interlocked.Exchange
Performance:
- Cache hit: O(1) - dictionary lookup
- Cache miss: O(1) - dictionary insert
- Eviction: O(n) - scans all entries
Component Type Cache - FIFO (First In First Out)
Why FIFO?
- Component types rarely change
- First loaded components are usually most important
- Simple and predictable
How it works:
ConcurrentQueuetracks insertion order- When cache is full, oldest entry is removed
- Dictionary entry is removed correspondingly
Best Practices
When to Enable Caching
✅ Enable for:
- Static/public content (homepage, about, terms)
- Navigation-heavy applications
- Complex nested routes
- Frequently accessed pages
- Lazy-loaded components
When to Disable Caching
❌ Disable for:
- User-specific content (profiles, dashboards)
- Real-time data (live feeds, chat)
- Admin panels with dynamic configuration
- Routes that must always execute guards/middleware fresh
Cache Tuning Guidelines
Cache Size:
// Small app (10-20 routes)
options.MaxRouteMatchCacheSize = 50;
// Medium app (50-100 routes)
options.MaxRouteMatchCacheSize = 100; // Default
// Large app (200+ routes)
options.MaxRouteMatchCacheSize = 300-500;
TTL Settings:
// Production - no expiration
options.RouteMatchCacheTTLSeconds = 0; // Default
// Development - hot reload
options.RouteMatchCacheTTLSeconds = 60; // 1 minute
// Staging - occasional changes
options.RouteMatchCacheTTLSeconds = 300; // 5 minutes
Statistics:
// Production - disabled (performance)
options.EnableStatistics = false; // Default
// Development/Staging - enabled (monitoring)
options.EnableStatistics = true;
Examples
Example 1: Basic Setup
// Program.cs
builder.Services.AddBlazouter();
// Default configuration:
// - Route match cache: 100 entries
// - Component type cache: 50 entries
// - No expiration
// - Statistics disabled
Example 2: Custom Configuration
builder.Services.AddBlazouter(options =>
{
options.MaxRouteMatchCacheSize = 500;
options.MaxComponentTypeCacheSize = 200;
options.EnableStatistics = true;
options.RouteMatchCacheTTLSeconds = 60;
});
Example 3: Cache Monitoring Component
@page "/cache-stats"
@inject IRouteCacheService CacheService
<h1>Cache Statistics</h1>
<div class="stats">
<p>Hit Rate: @_stats?.HitRate.ToString("F2")%</p>
<p>Total Requests: @_stats?.TotalRequests</p>
<p>Cache Hits: @_stats?.CacheHits</p>
<p>Cache Misses: @_stats?.CacheMisses</p>
</div>
<button @onclick="RefreshStats">Refresh</button>
<button @onclick="ClearCache">Clear Cache</button>
@code {
private CacheStatistics? _stats;
protected override void OnInitialized() => RefreshStats();
private void RefreshStats()
{
_stats = CacheService.GetStatistics();
StateHasChanged();
}
private void ClearCache()
{
CacheService.Clear();
RefreshStats();
}
}
Example 4: Selective Caching Strategy
private List<RouteConfig> _routes = new()
{
// Public static pages - aggressive caching
new RouteConfig
{
Path = "/",
Component = typeof(Home),
EnableCache = true
},
// User-specific pages - no caching
new RouteConfig
{
Path = "/profile",
Component = typeof(UserProfile),
EnableCache = false,
Guards = new List<Type> { typeof(AuthGuard) }
},
// Product catalog - cache with middleware
new RouteConfig
{
Path = "/products/:id",
Component = typeof(ProductDetail),
EnableCache = true,
Middleware = new List<Type> { typeof(AnalyticsMiddleware) }
}
};
Cache System Summary
| Feature | Route Match Cache | Component Type Cache |
|---|---|---|
| Status | ✅ Active | ✅ Active |
| Purpose | Cache route matching results | Cache lazy-loaded component types |
| Key | URL path (with query string) | Route path |
| Value | RouteMatch object | Component Type |
| Eviction | LRU (Least Recently Used) | FIFO (First In First Out) |
| Default Size | 100 entries | 50 entries |
| TTL Support | ✅ Yes | ❌ No |
| Thread Safety | ✅ ConcurrentDictionary | ✅ ConcurrentDictionary + Queue |
| Memory Impact | ~20-50 KB | ~5-10 KB |
| Performance Gain | 10-100x | Instant after first load |
| Per-Route Control | ✅ RouteConfig.EnableCache | ❌ Global only |
| Integration | ✅ CachedRouteMatcherService | ✅ Router component |
Implementation Details
Route Match Cache Integration
The CachedRouteMatcherService wraps the standard RouteMatcherService:
public class CachedRouteMatcherService : IRouteMatcherService
{
private readonly RouteMatcherService _innerMatcher;
private readonly IRouteCacheService _cacheService;
public RouteMatch? MatchRoute(string path, List<RouteConfig> routes)
{
// Try cache first
var cachedMatch = _cacheService.GetCachedRouteMatch(path);
if (cachedMatch != null)
{
if (cachedMatch.Route?.EnableCache == false)
{
_cacheService.InvalidateRouteMatch(path);
}
else
{
return cachedMatch; // Cache hit!
}
}
// Cache miss - perform matching
var match = _innerMatcher.MatchRoute(path, routes);
// Cache successful matches
if (match != null && (match.Route?.EnableCache ?? true))
{
_cacheService.CacheRouteMatch(path, match);
}
return match;
}
}
Component Type Cache Integration
The Router component uses cache for lazy loading:
private async Task LoadComponentWithCacheAsync(RouteMatch matchToLoad)
{
// Check cache first
Type? cachedType = CacheService.GetCachedComponentType(matchToLoad.Route.Path);
if (cachedType != null)
{
// Cache hit - instant!
matchToLoad.ComponentType = cachedType;
}
else
{
// Cache miss - load component
matchToLoad.ComponentType = await matchToLoad.Route.ComponentLoader!();
// Store in cache
if (matchToLoad.ComponentType != null)
{
CacheService.CacheComponentType(matchToLoad.Route.Path, matchToLoad.ComponentType);
}
}
}
Performance Impact
Before Caching
- Every navigation: Route matching algorithm runs (1-10ms)
- Every lazy load: ComponentLoader executes (async delay)
After Caching
Route Match Cache:
- First navigation: Normal (1-10ms)
- Subsequent navigations: Instant (~0.1ms)
- Performance gain: 10-100x
Component Type Cache:
- First lazy load: Normal (ComponentLoader executes)
- Subsequent lazy loads: Instant (cached type used)
- Performance gain: ∞ (async loading completely bypassed)
Troubleshooting
Cache Not Working
-
Check if caching is enabled:
options.EnableRouteMatchCache = true; options.EnableComponentTypeCache = true; -
Check per-route settings:
EnableCache = null // or true -
Verify statistics (if enabled):
var stats = CacheService.GetStatistics(); Console.WriteLine($"Hit Rate: {stats.HitRate}%");
Cache Growing Too Large
Adjust cache sizes:
options.MaxRouteMatchCacheSize = 50; // Reduce size
options.MaxComponentTypeCacheSize = 25;
Cache Entries Not Expiring
Set TTL if needed:
options.RouteMatchCacheTTLSeconds = 300; // 5 minutes
Conclusion
The Blazouter caching system provides:
- Transparent Performance: No code changes required
- Configurable Behavior: Fine-tune for your needs
- Significant Gains: 10-100x speedup for routes, instant lazy loading
- Thread-Safe: Safe for server-side scenarios
- Memory Efficient: LRU/FIFO eviction keeps memory bounded
- Flexible Control: Global and per-route configuration
The cache system is production-ready and provides substantial performance improvements, especially for applications with complex routing structures and lazy-loaded components.