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
|
// Redirect URL for invalid/missing token requests
|
||||||
const REDIRECT_URL = 'https://runmaestro.ai';
|
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
|
* Static Routes Class
|
||||||
*
|
*
|
||||||
@@ -77,7 +114,8 @@ export class StaticRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const indexPath = path.join(this.webAssetsPath, 'index.html');
|
const indexPath = path.join(this.webAssetsPath, 'index.html');
|
||||||
if (!existsSync(indexPath)) {
|
const cachedHtml = getCachedFile(indexPath);
|
||||||
|
if (cachedHtml === null) {
|
||||||
reply.code(404).send({
|
reply.code(404).send({
|
||||||
error: 'Not Found',
|
error: 'Not Found',
|
||||||
message: 'Web interface index.html not found.',
|
message: 'Web interface index.html not found.',
|
||||||
@@ -86,8 +124,8 @@ export class StaticRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read and transform the HTML to fix asset paths
|
// Use cached HTML and transform asset paths
|
||||||
let html = readFileSync(indexPath, 'utf-8');
|
let html = cachedHtml;
|
||||||
|
|
||||||
// Transform relative paths to use the token-prefixed absolute paths
|
// Transform relative paths to use the token-prefixed absolute paths
|
||||||
html = html.replace(/\.\/assets\//g, `/${this.securityToken}/assets/`);
|
html = html.replace(/\.\/assets\//g, `/${this.securityToken}/assets/`);
|
||||||
@@ -138,28 +176,30 @@ export class StaticRoutes {
|
|||||||
return { status: 'ok', timestamp: Date.now() };
|
return { status: 'ok', timestamp: Date.now() };
|
||||||
});
|
});
|
||||||
|
|
||||||
// PWA manifest.json
|
// PWA manifest.json (cached)
|
||||||
server.get(`/${token}/manifest.json`, async (_request, reply) => {
|
server.get(`/${token}/manifest.json`, async (_request, reply) => {
|
||||||
if (!this.webAssetsPath) {
|
if (!this.webAssetsPath) {
|
||||||
return reply.code(404).send({ error: 'Not Found' });
|
return reply.code(404).send({ error: 'Not Found' });
|
||||||
}
|
}
|
||||||
const manifestPath = path.join(this.webAssetsPath, 'manifest.json');
|
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.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) => {
|
server.get(`/${token}/sw.js`, async (_request, reply) => {
|
||||||
if (!this.webAssetsPath) {
|
if (!this.webAssetsPath) {
|
||||||
return reply.code(404).send({ error: 'Not Found' });
|
return reply.code(404).send({ error: 'Not Found' });
|
||||||
}
|
}
|
||||||
const swPath = path.join(this.webAssetsPath, 'sw.js');
|
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.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
|
// Dashboard - list all live sessions
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import React, {
|
|||||||
lazy,
|
lazy,
|
||||||
Suspense,
|
Suspense,
|
||||||
} from 'react';
|
} 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 { SessionList } from './components/SessionList';
|
||||||
import { RightPanel, RightPanelHandle } from './components/RightPanel';
|
import { RightPanel, RightPanelHandle } from './components/RightPanel';
|
||||||
import { slashCommands } from './slashCommands';
|
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 */}
|
{/* Old settings modal removed - using new SettingsModal component below */}
|
||||||
{/* NOTE: NewInstanceModal and EditAgentModal are now rendered via AppSessionModals */}
|
{/* NOTE: NewInstanceModal and EditAgentModal are now rendered via AppSessionModals */}
|
||||||
|
|
||||||
{/* --- SETTINGS MODAL (New Component) --- */}
|
{/* --- SETTINGS MODAL (Lazy-loaded for performance) --- */}
|
||||||
<SettingsModal
|
{settingsModalOpen && (
|
||||||
isOpen={settingsModalOpen}
|
<Suspense fallback={null}>
|
||||||
onClose={handleCloseSettings}
|
<SettingsModal
|
||||||
theme={theme}
|
isOpen={settingsModalOpen}
|
||||||
themes={THEMES}
|
onClose={handleCloseSettings}
|
||||||
activeThemeId={activeThemeId}
|
theme={theme}
|
||||||
setActiveThemeId={setActiveThemeId}
|
themes={THEMES}
|
||||||
customThemeColors={customThemeColors}
|
activeThemeId={activeThemeId}
|
||||||
setCustomThemeColors={setCustomThemeColors}
|
setActiveThemeId={setActiveThemeId}
|
||||||
customThemeBaseId={customThemeBaseId}
|
customThemeColors={customThemeColors}
|
||||||
setCustomThemeBaseId={setCustomThemeBaseId}
|
setCustomThemeColors={setCustomThemeColors}
|
||||||
llmProvider={llmProvider}
|
customThemeBaseId={customThemeBaseId}
|
||||||
setLlmProvider={setLlmProvider}
|
setCustomThemeBaseId={setCustomThemeBaseId}
|
||||||
modelSlug={modelSlug}
|
llmProvider={llmProvider}
|
||||||
setModelSlug={setModelSlug}
|
setLlmProvider={setLlmProvider}
|
||||||
apiKey={apiKey}
|
modelSlug={modelSlug}
|
||||||
setApiKey={setApiKey}
|
setModelSlug={setModelSlug}
|
||||||
shortcuts={shortcuts}
|
apiKey={apiKey}
|
||||||
setShortcuts={setShortcuts}
|
setApiKey={setApiKey}
|
||||||
tabShortcuts={tabShortcuts}
|
shortcuts={shortcuts}
|
||||||
setTabShortcuts={setTabShortcuts}
|
setShortcuts={setShortcuts}
|
||||||
defaultShell={defaultShell}
|
tabShortcuts={tabShortcuts}
|
||||||
setDefaultShell={setDefaultShell}
|
setTabShortcuts={setTabShortcuts}
|
||||||
customShellPath={customShellPath}
|
defaultShell={defaultShell}
|
||||||
setCustomShellPath={setCustomShellPath}
|
setDefaultShell={setDefaultShell}
|
||||||
shellArgs={shellArgs}
|
customShellPath={customShellPath}
|
||||||
setShellArgs={setShellArgs}
|
setCustomShellPath={setCustomShellPath}
|
||||||
shellEnvVars={shellEnvVars}
|
shellArgs={shellArgs}
|
||||||
setShellEnvVars={setShellEnvVars}
|
setShellArgs={setShellArgs}
|
||||||
ghPath={ghPath}
|
shellEnvVars={shellEnvVars}
|
||||||
setGhPath={setGhPath}
|
setShellEnvVars={setShellEnvVars}
|
||||||
enterToSendAI={enterToSendAI}
|
ghPath={ghPath}
|
||||||
setEnterToSendAI={setEnterToSendAI}
|
setGhPath={setGhPath}
|
||||||
enterToSendTerminal={enterToSendTerminal}
|
enterToSendAI={enterToSendAI}
|
||||||
setEnterToSendTerminal={setEnterToSendTerminal}
|
setEnterToSendAI={setEnterToSendAI}
|
||||||
defaultSaveToHistory={defaultSaveToHistory}
|
enterToSendTerminal={enterToSendTerminal}
|
||||||
setDefaultSaveToHistory={setDefaultSaveToHistory}
|
setEnterToSendTerminal={setEnterToSendTerminal}
|
||||||
defaultShowThinking={defaultShowThinking}
|
defaultSaveToHistory={defaultSaveToHistory}
|
||||||
setDefaultShowThinking={setDefaultShowThinking}
|
setDefaultSaveToHistory={setDefaultSaveToHistory}
|
||||||
fontFamily={fontFamily}
|
defaultShowThinking={defaultShowThinking}
|
||||||
setFontFamily={setFontFamily}
|
setDefaultShowThinking={setDefaultShowThinking}
|
||||||
fontSize={fontSize}
|
fontFamily={fontFamily}
|
||||||
setFontSize={setFontSize}
|
setFontFamily={setFontFamily}
|
||||||
terminalWidth={terminalWidth}
|
fontSize={fontSize}
|
||||||
setTerminalWidth={setTerminalWidth}
|
setFontSize={setFontSize}
|
||||||
logLevel={logLevel}
|
terminalWidth={terminalWidth}
|
||||||
setLogLevel={setLogLevel}
|
setTerminalWidth={setTerminalWidth}
|
||||||
maxLogBuffer={maxLogBuffer}
|
logLevel={logLevel}
|
||||||
setMaxLogBuffer={setMaxLogBuffer}
|
setLogLevel={setLogLevel}
|
||||||
maxOutputLines={maxOutputLines}
|
maxLogBuffer={maxLogBuffer}
|
||||||
setMaxOutputLines={setMaxOutputLines}
|
setMaxLogBuffer={setMaxLogBuffer}
|
||||||
osNotificationsEnabled={osNotificationsEnabled}
|
maxOutputLines={maxOutputLines}
|
||||||
setOsNotificationsEnabled={setOsNotificationsEnabled}
|
setMaxOutputLines={setMaxOutputLines}
|
||||||
audioFeedbackEnabled={audioFeedbackEnabled}
|
osNotificationsEnabled={osNotificationsEnabled}
|
||||||
setAudioFeedbackEnabled={setAudioFeedbackEnabled}
|
setOsNotificationsEnabled={setOsNotificationsEnabled}
|
||||||
audioFeedbackCommand={audioFeedbackCommand}
|
audioFeedbackEnabled={audioFeedbackEnabled}
|
||||||
setAudioFeedbackCommand={setAudioFeedbackCommand}
|
setAudioFeedbackEnabled={setAudioFeedbackEnabled}
|
||||||
toastDuration={toastDuration}
|
audioFeedbackCommand={audioFeedbackCommand}
|
||||||
setToastDuration={setToastDuration}
|
setAudioFeedbackCommand={setAudioFeedbackCommand}
|
||||||
checkForUpdatesOnStartup={checkForUpdatesOnStartup}
|
toastDuration={toastDuration}
|
||||||
setCheckForUpdatesOnStartup={setCheckForUpdatesOnStartup}
|
setToastDuration={setToastDuration}
|
||||||
enableBetaUpdates={enableBetaUpdates}
|
checkForUpdatesOnStartup={checkForUpdatesOnStartup}
|
||||||
setEnableBetaUpdates={setEnableBetaUpdates}
|
setCheckForUpdatesOnStartup={setCheckForUpdatesOnStartup}
|
||||||
crashReportingEnabled={crashReportingEnabled}
|
enableBetaUpdates={enableBetaUpdates}
|
||||||
setCrashReportingEnabled={setCrashReportingEnabled}
|
setEnableBetaUpdates={setEnableBetaUpdates}
|
||||||
customAICommands={customAICommands}
|
crashReportingEnabled={crashReportingEnabled}
|
||||||
setCustomAICommands={setCustomAICommands}
|
setCrashReportingEnabled={setCrashReportingEnabled}
|
||||||
initialTab={settingsTab}
|
customAICommands={customAICommands}
|
||||||
hasNoAgents={hasNoAgents}
|
setCustomAICommands={setCustomAICommands}
|
||||||
onThemeImportError={(msg) => setFlashNotification(msg)}
|
initialTab={settingsTab}
|
||||||
onThemeImportSuccess={(msg) => setFlashNotification(msg)}
|
hasNoAgents={hasNoAgents}
|
||||||
/>
|
onThemeImportError={(msg) => setFlashNotification(msg)}
|
||||||
|
onThemeImportSuccess={(msg) => setFlashNotification(msg)}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* --- WIZARD RESUME MODAL (asks if user wants to resume incomplete wizard) --- */}
|
{/* --- WIZARD RESUME MODAL (asks if user wants to resume incomplete wizard) --- */}
|
||||||
{wizardResumeModalOpen && wizardResumeState && (
|
{wizardResumeModalOpen && wizardResumeState && (
|
||||||
|
|||||||
@@ -49,6 +49,70 @@ export default defineConfig(({ mode }) => ({
|
|||||||
build: {
|
build: {
|
||||||
outDir: path.join(__dirname, 'dist/renderer'),
|
outDir: path.join(__dirname, 'dist/renderer'),
|
||||||
emptyOutDir: true,
|
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: {
|
server: {
|
||||||
port: process.env.VITE_PORT ? parseInt(process.env.VITE_PORT, 10) : 5173,
|
port: process.env.VITE_PORT ? parseInt(process.env.VITE_PORT, 10) : 5173,
|
||||||
|
|||||||
Reference in New Issue
Block a user