diff --git a/src/main/web-server/routes/staticRoutes.ts b/src/main/web-server/routes/staticRoutes.ts index 135be5b7..bc2e047f 100644 --- a/src/main/web-server/routes/staticRoutes.ts +++ b/src/main/web-server/routes/staticRoutes.ts @@ -25,6 +25,43 @@ const LOG_CONTEXT = 'WebServer:Static'; // Redirect URL for invalid/missing token requests const REDIRECT_URL = 'https://runmaestro.ai'; +/** + * File cache for static assets that don't change at runtime. + * Prevents blocking file reads on every request. + */ +interface CachedFile { + content: string; + exists: boolean; +} + +const fileCache = new Map(); + +/** + * Read a file with caching - only reads from disk once per path. + * Returns null if file doesn't exist. + */ +function getCachedFile(filePath: string): string | null { + const cached = fileCache.get(filePath); + if (cached !== undefined) { + return cached.exists ? cached.content : null; + } + + // First access - read from disk and cache + if (!existsSync(filePath)) { + fileCache.set(filePath, { content: '', exists: false }); + return null; + } + + try { + const content = readFileSync(filePath, 'utf-8'); + fileCache.set(filePath, { content, exists: true }); + return content; + } catch { + fileCache.set(filePath, { content: '', exists: false }); + return null; + } +} + /** * Static Routes Class * @@ -77,7 +114,8 @@ export class StaticRoutes { } const indexPath = path.join(this.webAssetsPath, 'index.html'); - if (!existsSync(indexPath)) { + const cachedHtml = getCachedFile(indexPath); + if (cachedHtml === null) { reply.code(404).send({ error: 'Not Found', message: 'Web interface index.html not found.', @@ -86,8 +124,8 @@ export class StaticRoutes { } try { - // Read and transform the HTML to fix asset paths - let html = readFileSync(indexPath, 'utf-8'); + // Use cached HTML and transform asset paths + let html = cachedHtml; // Transform relative paths to use the token-prefixed absolute paths html = html.replace(/\.\/assets\//g, `/${this.securityToken}/assets/`); @@ -138,28 +176,30 @@ export class StaticRoutes { return { status: 'ok', timestamp: Date.now() }; }); - // PWA manifest.json + // PWA manifest.json (cached) server.get(`/${token}/manifest.json`, async (_request, reply) => { if (!this.webAssetsPath) { return reply.code(404).send({ error: 'Not Found' }); } const manifestPath = path.join(this.webAssetsPath, 'manifest.json'); - if (!existsSync(manifestPath)) { + const content = getCachedFile(manifestPath); + if (content === null) { return reply.code(404).send({ error: 'Not Found' }); } - return reply.type('application/json').send(readFileSync(manifestPath, 'utf-8')); + return reply.type('application/json').send(content); }); - // PWA service worker + // PWA service worker (cached) server.get(`/${token}/sw.js`, async (_request, reply) => { if (!this.webAssetsPath) { return reply.code(404).send({ error: 'Not Found' }); } const swPath = path.join(this.webAssetsPath, 'sw.js'); - if (!existsSync(swPath)) { + const content = getCachedFile(swPath); + if (content === null) { return reply.code(404).send({ error: 'Not Found' }); } - return reply.type('application/javascript').send(readFileSync(swPath, 'utf-8')); + return reply.type('application/javascript').send(content); }); // Dashboard - list all live sessions diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b26792b8..8d59c9b5 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -8,7 +8,10 @@ import React, { lazy, Suspense, } from 'react'; -import { SettingsModal } from './components/SettingsModal'; +// SettingsModal is lazy-loaded for performance (large component, only loaded when settings opened) +const SettingsModal = lazy(() => + import('./components/SettingsModal').then((m) => ({ default: m.SettingsModal })) +); import { SessionList } from './components/SessionList'; import { RightPanel, RightPanelHandle } from './components/RightPanel'; import { slashCommands } from './slashCommands'; @@ -14326,79 +14329,83 @@ You are taking over this conversation. Based on the context above, provide a bri {/* Old settings modal removed - using new SettingsModal component below */} {/* NOTE: NewInstanceModal and EditAgentModal are now rendered via AppSessionModals */} - {/* --- SETTINGS MODAL (New Component) --- */} - setFlashNotification(msg)} - onThemeImportSuccess={(msg) => setFlashNotification(msg)} - /> + {/* --- SETTINGS MODAL (Lazy-loaded for performance) --- */} + {settingsModalOpen && ( + + setFlashNotification(msg)} + onThemeImportSuccess={(msg) => setFlashNotification(msg)} + /> + + )} {/* --- WIZARD RESUME MODAL (asks if user wants to resume incomplete wizard) --- */} {wizardResumeModalOpen && wizardResumeState && ( diff --git a/vite.config.mts b/vite.config.mts index 1f59231e..2d95d82b 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -49,6 +49,70 @@ export default defineConfig(({ mode }) => ({ build: { outDir: path.join(__dirname, 'dist/renderer'), emptyOutDir: true, + rollupOptions: { + output: { + // Manual chunking for better caching and code splitting + manualChunks: (id) => { + // React core in its own chunk for optimal caching + if (id.includes('node_modules/react-dom')) { + return 'vendor-react'; + } + if (id.includes('node_modules/react/') || id.includes('node_modules/react-is')) { + return 'vendor-react'; + } + if (id.includes('node_modules/scheduler')) { + return 'vendor-react'; + } + + // Terminal (xterm) in its own chunk - large and not immediately needed + if (id.includes('node_modules/xterm')) { + return 'vendor-xterm'; + } + + // Markdown processing libraries + if ( + id.includes('node_modules/react-markdown') || + id.includes('node_modules/remark-') || + id.includes('node_modules/rehype-') || + id.includes('node_modules/unified') || + id.includes('node_modules/unist-') || + id.includes('node_modules/mdast-') || + id.includes('node_modules/hast-') || + id.includes('node_modules/micromark') + ) { + return 'vendor-markdown'; + } + + // Syntax highlighting (large) + if ( + id.includes('node_modules/react-syntax-highlighter') || + id.includes('node_modules/prismjs') || + id.includes('node_modules/refractor') + ) { + return 'vendor-syntax'; + } + + // Heavy visualization libraries (lazy-loaded components) + if (id.includes('node_modules/mermaid')) { + return 'vendor-mermaid'; + } + if (id.includes('node_modules/recharts') || id.includes('node_modules/d3-')) { + return 'vendor-charts'; + } + if (id.includes('node_modules/reactflow') || id.includes('node_modules/@reactflow')) { + return 'vendor-flow'; + } + + // Diff viewer + if (id.includes('node_modules/react-diff-view') || id.includes('node_modules/diff')) { + return 'vendor-diff'; + } + + // Return undefined to let Rollup handle other modules automatically + return undefined; + }, + }, + }, }, server: { port: process.env.VITE_PORT ? parseInt(process.env.VITE_PORT, 10) : 5173,