mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
perf: improve startup and runtime performance
Tier 1 performance optimizations for snappier app experience: 1. Lazy-load SettingsModal in App.tsx - SettingsModal is ~2.6K lines, now only loaded when settings opened - Wrapped in Suspense with conditional rendering 2. Add manual chunk splitting to vite.config.mts - Split vendor chunks: react, xterm, markdown, syntax, mermaid, charts, flow, diff - Enables better browser caching and smaller initial bundle - Heavy visualization libs (mermaid ~500KB, recharts ~400KB, etc.) now in separate chunks 3. Cache static files in web server (staticRoutes.ts) - index.html, manifest.json, sw.js now read once and cached - Eliminates blocking sync file reads on every web request - Improves web/mobile interface responsiveness
This commit is contained in:
@@ -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<string, CachedFile>();
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -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,7 +14329,9 @@ 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) --- */}
|
||||
{/* --- SETTINGS MODAL (Lazy-loaded for performance) --- */}
|
||||
{settingsModalOpen && (
|
||||
<Suspense fallback={null}>
|
||||
<SettingsModal
|
||||
isOpen={settingsModalOpen}
|
||||
onClose={handleCloseSettings}
|
||||
@@ -14399,6 +14404,8 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
onThemeImportError={(msg) => setFlashNotification(msg)}
|
||||
onThemeImportSuccess={(msg) => setFlashNotification(msg)}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{/* --- WIZARD RESUME MODAL (asks if user wants to resume incomplete wizard) --- */}
|
||||
{wizardResumeModalOpen && wizardResumeState && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user