From d4e81e4478a09cb6faf2032d5e03a74d8a18eb20 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 13 Oct 2025 11:39:54 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20Lazy-load=20IpcMain=20to=20r?= =?UTF-8?q?educe=20startup=20time=20&=20lowercase=20package=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem:** App startup took 6+ seconds for Electron users because main.ts immediately loaded IpcMain, which transitively imports the entire AI SDK stack (ai, @ai-sdk/anthropic, ai-tokenizer, etc.) - heavy modules that weren't needed until the window was created and user started interacting. Additionally, the package was being built as "Cmux" instead of "cmux". **Solution:** 1. **Lazy-load IpcMain**: Convert static imports to dynamic imports in createWindow(). Config, IpcMain, and tokenizer modules now load on-demand when the window is created, not at app startup. 2. **Lowercase package name**: Changed productName from "Cmux" to "cmux" in package.json build config. **Implementation details:** - Changed Config/IpcMain/loadTokenizerModules imports to type-only imports - Created module-level variables to cache loaded modules - Made createWindow() async to await dynamic imports - Moved tokenizer loading to after window creation - Fixed e2e test userData path (can't use config.rootDir before config loads) - Added ESLint justification for dynamic imports **Testing:** - Unit tests pass (379 tests) - Type checking passes - ESLint passes --- package.json | 2 +- src/main.ts | 60 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 428558a840..002081281c 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ }, "build": { "appId": "com.cmux.app", - "productName": "Cmux", + "productName": "cmux", "publish": { "provider": "github", "releaseType": "release" diff --git a/src/main.ts b/src/main.ts index c9deace2b2..6a80d8ce17 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,10 +5,10 @@ import type { MenuItemConstructorOptions } from "electron"; import { app, BrowserWindow, ipcMain as electronIpcMain, Menu, shell, dialog } from "electron"; import * as fs from "fs"; import * as path from "path"; -import { Config } from "./config"; -import { IpcMain } from "./services/ipcMain"; +import type { Config } from "./config"; +import type { IpcMain } from "./services/ipcMain"; import { VERSION } from "./version"; -import { loadTokenizerModules } from "./utils/main/tokenizer"; +import type { loadTokenizerModules } from "./utils/main/tokenizer"; // React DevTools for development profiling // Using require() instead of import since it's dev-only and conditionally loaded @@ -39,13 +39,19 @@ if (!app.isPackaged) { } } -const config = new Config(); -const ipcMain = new IpcMain(config); +// Lazy-load Config and IpcMain to avoid loading heavy AI SDK dependencies at startup +// These will be loaded on-demand when createWindow() is called +let config: Config | null = null; +let ipcMain: IpcMain | null = null; +let loadTokenizerModulesFn: typeof loadTokenizerModules | null = null; const isE2ETest = process.env.CMUX_E2E === "1"; const forceDistLoad = process.env.CMUX_E2E_LOAD_DIST === "1"; if (isE2ETest) { - const e2eUserData = path.join(config.rootDir, "user-data"); + // For e2e tests, use a test-specific userData directory + // Note: We can't use config.rootDir here because config isn't loaded yet + // Instead, we'll use a hardcoded path relative to home directory + const e2eUserData = path.join(process.env.HOME ?? "~", ".cmux", "user-data"); try { fs.mkdirSync(e2eUserData, { recursive: true }); app.setPath("userData", e2eUserData); @@ -175,7 +181,27 @@ function createMenu() { Menu.setApplicationMenu(menu); } -function createWindow() { +async function createWindow() { + // Lazy-load Config and IpcMain only when window is created + // This defers loading heavy AI SDK dependencies until actually needed + if (!config || !ipcMain || !loadTokenizerModulesFn) { + /* eslint-disable no-restricted-syntax */ + // Dynamic imports are justified here for performance: + // - IpcMain transitively imports the entire AI SDK (ai, @ai-sdk/anthropic, etc.) + // - These are large modules that would block app startup if loaded statically + // - Loading happens once on first window creation, then cached + const [{ Config: ConfigClass }, { IpcMain: IpcMainClass }, { loadTokenizerModules: loadTokenizerFn }] = + await Promise.all([ + import("./config"), + import("./services/ipcMain"), + import("./utils/main/tokenizer"), + ]); + /* eslint-enable no-restricted-syntax */ + config = new ConfigClass(); + ipcMain = new IpcMainClass(config); + loadTokenizerModulesFn = loadTokenizerFn; + } + mainWindow = new BrowserWindow({ width: 1200, height: 800, @@ -235,13 +261,6 @@ if (gotTheLock) { void app.whenReady().then(async () => { console.log("App ready, creating window..."); - // Start loading tokenizer modules in background - // This ensures accurate token counts for first API calls (especially in e2e tests) - // Loading happens asynchronously and won't block window creation - void loadTokenizerModules().then(() => { - console.log("Tokenizer modules loaded"); - }); - // Install React DevTools in development if (!app.isPackaged && installExtension && REACT_DEVELOPER_TOOLS) { try { @@ -255,7 +274,16 @@ if (gotTheLock) { } createMenu(); - createWindow(); + await createWindow(); + + // Start loading tokenizer modules in background after window is created + // This ensures accurate token counts for first API calls (especially in e2e tests) + // Loading happens asynchronously and won't block the UI + if (loadTokenizerModulesFn) { + void loadTokenizerModulesFn().then(() => { + console.log("Tokenizer modules loaded"); + }); + } // No need to auto-start workspaces anymore - they start on demand }); @@ -269,7 +297,7 @@ if (gotTheLock) { // Only create window if app is ready and no window exists // This prevents "Cannot create BrowserWindow before app is ready" error if (app.isReady() && mainWindow === null) { - createWindow(); + void createWindow(); } }); } From 2df4d8a52827d7b571e2fd312c21e7608576de8d Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 13 Oct 2025 11:41:54 -0500 Subject: [PATCH 2/3] Apply formatting --- src/main.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main.ts b/src/main.ts index 6a80d8ce17..f85d21fb57 100644 --- a/src/main.ts +++ b/src/main.ts @@ -190,12 +190,15 @@ async function createWindow() { // - IpcMain transitively imports the entire AI SDK (ai, @ai-sdk/anthropic, etc.) // - These are large modules that would block app startup if loaded statically // - Loading happens once on first window creation, then cached - const [{ Config: ConfigClass }, { IpcMain: IpcMainClass }, { loadTokenizerModules: loadTokenizerFn }] = - await Promise.all([ - import("./config"), - import("./services/ipcMain"), - import("./utils/main/tokenizer"), - ]); + const [ + { Config: ConfigClass }, + { IpcMain: IpcMainClass }, + { loadTokenizerModules: loadTokenizerFn }, + ] = await Promise.all([ + import("./config"), + import("./services/ipcMain"), + import("./utils/main/tokenizer"), + ]); /* eslint-enable no-restricted-syntax */ config = new ConfigClass(); ipcMain = new IpcMainClass(config); From f3a44ca5b1fe4a7710f4457b48fafb1e2ff68574 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 13 Oct 2025 11:44:20 -0500 Subject: [PATCH 3/3] Respect CMUX_TEST_ROOT for e2e test isolation --- src/main.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index f85d21fb57..4a86d96ad5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -50,8 +50,9 @@ const forceDistLoad = process.env.CMUX_E2E_LOAD_DIST === "1"; if (isE2ETest) { // For e2e tests, use a test-specific userData directory // Note: We can't use config.rootDir here because config isn't loaded yet - // Instead, we'll use a hardcoded path relative to home directory - const e2eUserData = path.join(process.env.HOME ?? "~", ".cmux", "user-data"); + // However, we must respect CMUX_TEST_ROOT to maintain test isolation + const testRoot = process.env.CMUX_TEST_ROOT ?? path.join(process.env.HOME ?? "~", ".cmux"); + const e2eUserData = path.join(testRoot, "user-data"); try { fs.mkdirSync(e2eUserData, { recursive: true }); app.setPath("userData", e2eUserData);