feat(perf): add cache warmup for improved performance

Implement automatic cache warming when users first connect to views.
Pre-renders all URLs in a view to eliminate initial load latency.
Also adds periodic warmup for active devices and removes cache TTL
refresh on cache hits to preserve original expiration timing.
This commit is contained in:
2026-05-13 13:17:57 +02:00
parent 69b62b6af3
commit daee65e0d5
+70 -2
View File
@@ -16,6 +16,7 @@ const CONFIG = {
MAX_CONCURRENT_RENDERS: 16,
QUEUE_TIMEOUT_MS: 30000,
CACHE_CLEANUP_INTERVAL_MS: 30000,
WARMUP_INTERVAL_MS: 60000,
} as const;
// ═══════════════════════════════════════════════════════════════
@@ -215,6 +216,7 @@ function viewCacheAll(): View[] {
// ═══════════════════════════════════════════════════════════════
const devices = new Map<string, DeviceEntry>();
const warmedUpViews = new Set<string>();
const DEVICE_TTL_MS = 120_000;
function deviceTrack(ip: string, viewId: string, viewName: string, userAgent: string): void {
@@ -227,6 +229,14 @@ function deviceTrack(ip: string, viewId: string, viewName: string, userAgent: st
existing.requestCount++;
} else {
devices.set(ip, { ip, viewId, viewName, userAgent, lastSeen: Date.now(), requestCount: 1 });
// First user connecting to this view - trigger warmup
if (!warmedUpViews.has(viewId)) {
warmedUpViews.add(viewId);
warmupView(viewId).catch(err => {
console.error(`[warmup] Failed to warm up view ${viewId}:`, err);
});
}
}
}
@@ -467,6 +477,64 @@ function cachePurgeExpired(): void {
setInterval(cachePurgeExpired, CONFIG.CACHE_CLEANUP_INTERVAL_MS);
async function warmupView(viewId: string): Promise<void> {
const view = viewCacheGet(viewId);
if (!view) {
console.warn(`[warmup] View ${viewId} not found`);
return;
}
console.log(`[warmup] Starting precache for view "${view.name}" (${view.urls.length} URLs)`);
for (let i = 0; i < view.urls.length; i++) {
const key = renderCacheKey(view.id, i);
const cached = renderCache.get(key);
const now = Date.now();
if (!cached || now >= cached.expiresAt) {
try {
await getOrRender(view, i);
console.log(`[warmup] Precached ${view.name} URL ${i}: ${view.urls[i].url}`);
} catch (err: any) {
console.error(`[warmup] Failed to precache ${view.name} URL ${i}: ${err.message}`);
}
}
}
console.log(`[warmup] Precache complete for view "${view.name}"`);
}
async function warmupCache(): Promise<void> {
const activeDevices = devicesSnapshot();
if (activeDevices.length === 0) {
return;
}
console.log(`[warmup] Starting periodic precache for ${activeDevices.length} active device(s)`);
const views = viewCacheAll();
for (const view of views) {
for (let i = 0; i < view.urls.length; i++) {
const key = renderCacheKey(view.id, i);
const cached = renderCache.get(key);
const now = Date.now();
if (!cached || now >= cached.expiresAt) {
try {
await getOrRender(view, i);
console.log(`[warmup] Precached ${view.name} URL ${i}: ${view.urls[i].url}`);
} catch (err: any) {
console.error(`[warmup] Failed to precache ${view.name} URL ${i}: ${err.message}`);
}
}
}
}
console.log(`[warmup] Periodic precache complete`);
}
setInterval(warmupCache, CONFIG.WARMUP_INTERVAL_MS);
function cacheInvalidate(viewId: string): void {
const prefix = `${viewId}:`;
for (const key of renderCache.keys()) {
@@ -478,6 +546,8 @@ function cacheInvalidate(viewId: string): void {
renderQueue.splice(i, 1);
}
}
// Allow re-warming if view is recreated
warmedUpViews.delete(viewId);
}
function drainRenderQueue(): void {
@@ -504,7 +574,6 @@ function drainRenderQueue(): void {
const cached = renderCache.get(next.key);
if (cached && Date.now() < cached.expiresAt) {
cached.expiresAt = Date.now() + view.cacheTtlSec * 1000;
next.resolve(cached.body);
continue;
}
@@ -564,7 +633,6 @@ async function getOrRender(view: View, urlIndex: number): Promise<string> {
const cached = renderCache.get(key);
if (cached && now < cached.expiresAt) {
cached.expiresAt = now + view.cacheTtlSec * 1000;
return cached.body;
}