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:
Pedram Amini
2026-02-02 20:46:09 -06:00
parent 2fd83bdc4a
commit 03d157ee54
3 changed files with 194 additions and 83 deletions

View File

@@ -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

View File

@@ -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) --- */}
<SettingsModal
isOpen={settingsModalOpen}
onClose={handleCloseSettings}
theme={theme}
themes={THEMES}
activeThemeId={activeThemeId}
setActiveThemeId={setActiveThemeId}
customThemeColors={customThemeColors}
setCustomThemeColors={setCustomThemeColors}
customThemeBaseId={customThemeBaseId}
setCustomThemeBaseId={setCustomThemeBaseId}
llmProvider={llmProvider}
setLlmProvider={setLlmProvider}
modelSlug={modelSlug}
setModelSlug={setModelSlug}
apiKey={apiKey}
setApiKey={setApiKey}
shortcuts={shortcuts}
setShortcuts={setShortcuts}
tabShortcuts={tabShortcuts}
setTabShortcuts={setTabShortcuts}
defaultShell={defaultShell}
setDefaultShell={setDefaultShell}
customShellPath={customShellPath}
setCustomShellPath={setCustomShellPath}
shellArgs={shellArgs}
setShellArgs={setShellArgs}
shellEnvVars={shellEnvVars}
setShellEnvVars={setShellEnvVars}
ghPath={ghPath}
setGhPath={setGhPath}
enterToSendAI={enterToSendAI}
setEnterToSendAI={setEnterToSendAI}
enterToSendTerminal={enterToSendTerminal}
setEnterToSendTerminal={setEnterToSendTerminal}
defaultSaveToHistory={defaultSaveToHistory}
setDefaultSaveToHistory={setDefaultSaveToHistory}
defaultShowThinking={defaultShowThinking}
setDefaultShowThinking={setDefaultShowThinking}
fontFamily={fontFamily}
setFontFamily={setFontFamily}
fontSize={fontSize}
setFontSize={setFontSize}
terminalWidth={terminalWidth}
setTerminalWidth={setTerminalWidth}
logLevel={logLevel}
setLogLevel={setLogLevel}
maxLogBuffer={maxLogBuffer}
setMaxLogBuffer={setMaxLogBuffer}
maxOutputLines={maxOutputLines}
setMaxOutputLines={setMaxOutputLines}
osNotificationsEnabled={osNotificationsEnabled}
setOsNotificationsEnabled={setOsNotificationsEnabled}
audioFeedbackEnabled={audioFeedbackEnabled}
setAudioFeedbackEnabled={setAudioFeedbackEnabled}
audioFeedbackCommand={audioFeedbackCommand}
setAudioFeedbackCommand={setAudioFeedbackCommand}
toastDuration={toastDuration}
setToastDuration={setToastDuration}
checkForUpdatesOnStartup={checkForUpdatesOnStartup}
setCheckForUpdatesOnStartup={setCheckForUpdatesOnStartup}
enableBetaUpdates={enableBetaUpdates}
setEnableBetaUpdates={setEnableBetaUpdates}
crashReportingEnabled={crashReportingEnabled}
setCrashReportingEnabled={setCrashReportingEnabled}
customAICommands={customAICommands}
setCustomAICommands={setCustomAICommands}
initialTab={settingsTab}
hasNoAgents={hasNoAgents}
onThemeImportError={(msg) => setFlashNotification(msg)}
onThemeImportSuccess={(msg) => setFlashNotification(msg)}
/>
{/* --- SETTINGS MODAL (Lazy-loaded for performance) --- */}
{settingsModalOpen && (
<Suspense fallback={null}>
<SettingsModal
isOpen={settingsModalOpen}
onClose={handleCloseSettings}
theme={theme}
themes={THEMES}
activeThemeId={activeThemeId}
setActiveThemeId={setActiveThemeId}
customThemeColors={customThemeColors}
setCustomThemeColors={setCustomThemeColors}
customThemeBaseId={customThemeBaseId}
setCustomThemeBaseId={setCustomThemeBaseId}
llmProvider={llmProvider}
setLlmProvider={setLlmProvider}
modelSlug={modelSlug}
setModelSlug={setModelSlug}
apiKey={apiKey}
setApiKey={setApiKey}
shortcuts={shortcuts}
setShortcuts={setShortcuts}
tabShortcuts={tabShortcuts}
setTabShortcuts={setTabShortcuts}
defaultShell={defaultShell}
setDefaultShell={setDefaultShell}
customShellPath={customShellPath}
setCustomShellPath={setCustomShellPath}
shellArgs={shellArgs}
setShellArgs={setShellArgs}
shellEnvVars={shellEnvVars}
setShellEnvVars={setShellEnvVars}
ghPath={ghPath}
setGhPath={setGhPath}
enterToSendAI={enterToSendAI}
setEnterToSendAI={setEnterToSendAI}
enterToSendTerminal={enterToSendTerminal}
setEnterToSendTerminal={setEnterToSendTerminal}
defaultSaveToHistory={defaultSaveToHistory}
setDefaultSaveToHistory={setDefaultSaveToHistory}
defaultShowThinking={defaultShowThinking}
setDefaultShowThinking={setDefaultShowThinking}
fontFamily={fontFamily}
setFontFamily={setFontFamily}
fontSize={fontSize}
setFontSize={setFontSize}
terminalWidth={terminalWidth}
setTerminalWidth={setTerminalWidth}
logLevel={logLevel}
setLogLevel={setLogLevel}
maxLogBuffer={maxLogBuffer}
setMaxLogBuffer={setMaxLogBuffer}
maxOutputLines={maxOutputLines}
setMaxOutputLines={setMaxOutputLines}
osNotificationsEnabled={osNotificationsEnabled}
setOsNotificationsEnabled={setOsNotificationsEnabled}
audioFeedbackEnabled={audioFeedbackEnabled}
setAudioFeedbackEnabled={setAudioFeedbackEnabled}
audioFeedbackCommand={audioFeedbackCommand}
setAudioFeedbackCommand={setAudioFeedbackCommand}
toastDuration={toastDuration}
setToastDuration={setToastDuration}
checkForUpdatesOnStartup={checkForUpdatesOnStartup}
setCheckForUpdatesOnStartup={setCheckForUpdatesOnStartup}
enableBetaUpdates={enableBetaUpdates}
setEnableBetaUpdates={setEnableBetaUpdates}
crashReportingEnabled={crashReportingEnabled}
setCrashReportingEnabled={setCrashReportingEnabled}
customAICommands={customAICommands}
setCustomAICommands={setCustomAICommands}
initialTab={settingsTab}
hasNoAgents={hasNoAgents}
onThemeImportError={(msg) => setFlashNotification(msg)}
onThemeImportSuccess={(msg) => setFlashNotification(msg)}
/>
</Suspense>
)}
{/* --- WIZARD RESUME MODAL (asks if user wants to resume incomplete wizard) --- */}
{wizardResumeModalOpen && wizardResumeState && (

View File

@@ -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,