From 90630ff29c059daa9d6d0fe8b87e153016523676 Mon Sep 17 00:00:00 2001 From: iotserver24 <147928812+iotserver24@users.noreply.github.com> Date: Fri, 15 May 2026 17:36:08 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20[performance=20improvement]?= =?UTF-8?q?=20Async=20preview=20server=20file=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 3 ++ packages/desktop/src/main/preview-server.ts | 38 ++++++++++++++------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 9228b15..69da333 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -61,3 +61,6 @@ ## 2026-05-19 - Top-Level Interval Anti-Pattern (SpinnerVerb) **Learning:** Storing a slow interval state like `spinnerVerb` (every 2.4s) at the top-level `App` component still forces a full application tree O(N) cascade re-render every tick. Even if it is not as fast as a 250ms timer, it causes noticeable micro-stuttering during UI interactions and when large lists are rendered because it triggers diffs across the entire application including Heavy `ChatPanel` and `TabbedRightPanel`. **Action:** Always extract even slow interval-driven decorative states into isolated leaf components (like `SpinnerVerbDisplay`) wrapped in `React.memo()`. Removing the prop-drilling entirely isolates the re-renders specifically to the UI span rendering the changing string. +## 2024-05-15 - Electron Main Process File Operations +**Learning:** Using synchronous `fs` methods (`readFileSync`, `existsSync`, `statSync`) in the Electron main process (e.g., `preview-server.ts`) severely blocks the Node.js event loop, preventing concurrent processing of HTTP requests and potentially causing UI thread starvation or micro-stuttering. Furthermore, buffering entire large files (e.g., `readFileSync`) consumes unnecessary heap memory. +**Action:** Always default to async file operations (`fs.promises`) and stream piping (`createReadStream(..).pipe(res)`) when dealing with HTTP response bodies in the Electron main process to maintain responsiveness and minimize memory footprint. diff --git a/packages/desktop/src/main/preview-server.ts b/packages/desktop/src/main/preview-server.ts index 41aa0a7..b5fad13 100644 --- a/packages/desktop/src/main/preview-server.ts +++ b/packages/desktop/src/main/preview-server.ts @@ -22,6 +22,15 @@ const MIME_TYPES: Record = { '.wasm': 'application/wasm', }; +const existsAsync = async (p: string) => { + try { + await fs.promises.access(p); + return true; + } catch { + return false; + } +}; + export class PreviewServer { private server: Server | null = null; private port: number = 0; @@ -70,13 +79,14 @@ export class PreviewServer { return this.port ? `http://127.0.0.1:${this.port}` : ''; } - private handleRequest(req: any, res: any): void { + private async handleRequest(req: any, res: any): Promise { const urlPath = req.url.split('?')[0]; let filePath = path.join(this.rootDir, urlPath === '/' ? 'index.html' : urlPath); - if (!fs.existsSync(filePath)) { + // ⚡ Bolt: Replace synchronous file operations with async promises to prevent blocking the main thread + if (!(await existsAsync(filePath))) { const htmlPath = filePath + '.html'; - if (fs.existsSync(htmlPath)) { + if (await existsAsync(htmlPath)) { filePath = htmlPath; } else { res.writeHead(404); @@ -86,10 +96,10 @@ export class PreviewServer { } try { - const stat = fs.statSync(filePath); + const stat = await fs.promises.stat(filePath); if (stat.isDirectory()) { const indexPath = path.join(filePath, 'index.html'); - if (fs.existsSync(indexPath)) { + if (await existsAsync(indexPath)) { filePath = indexPath; } else { res.writeHead(403); @@ -106,16 +116,20 @@ export class PreviewServer { const ext = path.extname(filePath).toLowerCase(); const contentType = MIME_TYPES[ext] ?? 'application/octet-stream'; - try { - const content = fs.readFileSync(filePath); + // ⚡ Bolt: Use stream piping instead of readFileSync to avoid loading large files fully into memory + const readStream = fs.createReadStream(filePath); + readStream.on('error', () => { + if (!res.headersSent) { + res.writeHead(500); + } + res.end('Failed to read file'); + }); + readStream.on('open', () => { res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache', }); - res.end(content); - } catch { - res.writeHead(500); - res.end('Failed to read file'); - } + }); + readStream.pipe(res); } }