Skip to content

Commit db98e01

Browse files
authored
🤖 Lazy-load IpcMain to reduce startup time & lowercase package name (#223)
## 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.). These are large modules that weren't needed until the window was created and the 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. **Before:** ```typescript import { Config } from './config'; import { IpcMain } from './services/ipcMain'; const config = new Config(); const ipcMain = new IpcMain(config); // Heavy AI SDK loaded immediately ``` **After:** ```typescript import type { Config } from './config'; import type { IpcMain } from './services/ipcMain'; let config: Config | null = null; let ipcMain: IpcMain | null = null; async function createWindow() { if (!config || !ipcMain) { // Load only when needed const [{ Config: ConfigClass }, { IpcMain: IpcMainClass }] = await Promise.all([ import('./config'), import('./services/ipcMain'), ]); config = new ConfigClass(); ipcMain = new IpcMainClass(config); } // ... create window } ``` ### 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 _Generated with `cmux`_
1 parent acb2863 commit db98e01

File tree

2 files changed

+49
-17
lines changed

2 files changed

+49
-17
lines changed

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
},
103103
"build": {
104104
"appId": "com.cmux.app",
105-
"productName": "Cmux",
105+
"productName": "cmux",
106106
"publish": {
107107
"provider": "github",
108108
"releaseType": "release"

‎src/main.ts‎

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import type { MenuItemConstructorOptions } from "electron";
55
import { app, BrowserWindow, ipcMain as electronIpcMain, Menu, shell, dialog } from "electron";
66
import * as fs from "fs";
77
import * as path from "path";
8-
import { Config } from "./config";
9-
import { IpcMain } from "./services/ipcMain";
8+
import type { Config } from "./config";
9+
import type { IpcMain } from "./services/ipcMain";
1010
import { VERSION } from "./version";
11-
import { loadTokenizerModules } from "./utils/main/tokenizer";
11+
import type { loadTokenizerModules } from "./utils/main/tokenizer";
1212

1313
// React DevTools for development profiling
1414
// Using require() instead of import since it's dev-only and conditionally loaded
@@ -39,13 +39,20 @@ if (!app.isPackaged) {
3939
}
4040
}
4141

42-
const config = new Config();
43-
const ipcMain = new IpcMain(config);
42+
// Lazy-load Config and IpcMain to avoid loading heavy AI SDK dependencies at startup
43+
// These will be loaded on-demand when createWindow() is called
44+
let config: Config | null = null;
45+
let ipcMain: IpcMain | null = null;
46+
let loadTokenizerModulesFn: typeof loadTokenizerModules | null = null;
4447
const isE2ETest = process.env.CMUX_E2E === "1";
4548
const forceDistLoad = process.env.CMUX_E2E_LOAD_DIST === "1";
4649

4750
if (isE2ETest) {
48-
const e2eUserData = path.join(config.rootDir, "user-data");
51+
// For e2e tests, use a test-specific userData directory
52+
// Note: We can't use config.rootDir here because config isn't loaded yet
53+
// However, we must respect CMUX_TEST_ROOT to maintain test isolation
54+
const testRoot = process.env.CMUX_TEST_ROOT ?? path.join(process.env.HOME ?? "~", ".cmux");
55+
const e2eUserData = path.join(testRoot, "user-data");
4956
try {
5057
fs.mkdirSync(e2eUserData, { recursive: true });
5158
app.setPath("userData", e2eUserData);
@@ -175,7 +182,30 @@ function createMenu() {
175182
Menu.setApplicationMenu(menu);
176183
}
177184

178-
function createWindow() {
185+
async function createWindow() {
186+
// Lazy-load Config and IpcMain only when window is created
187+
// This defers loading heavy AI SDK dependencies until actually needed
188+
if (!config || !ipcMain || !loadTokenizerModulesFn) {
189+
/* eslint-disable no-restricted-syntax */
190+
// Dynamic imports are justified here for performance:
191+
// - IpcMain transitively imports the entire AI SDK (ai, @ai-sdk/anthropic, etc.)
192+
// - These are large modules that would block app startup if loaded statically
193+
// - Loading happens once on first window creation, then cached
194+
const [
195+
{ Config: ConfigClass },
196+
{ IpcMain: IpcMainClass },
197+
{ loadTokenizerModules: loadTokenizerFn },
198+
] = await Promise.all([
199+
import("./config"),
200+
import("./services/ipcMain"),
201+
import("./utils/main/tokenizer"),
202+
]);
203+
/* eslint-enable no-restricted-syntax */
204+
config = new ConfigClass();
205+
ipcMain = new IpcMainClass(config);
206+
loadTokenizerModulesFn = loadTokenizerFn;
207+
}
208+
179209
mainWindow = new BrowserWindow({
180210
width: 1200,
181211
height: 800,
@@ -235,13 +265,6 @@ if (gotTheLock) {
235265
void app.whenReady().then(async () => {
236266
console.log("App ready, creating window...");
237267

238-
// Start loading tokenizer modules in background
239-
// This ensures accurate token counts for first API calls (especially in e2e tests)
240-
// Loading happens asynchronously and won't block window creation
241-
void loadTokenizerModules().then(() => {
242-
console.log("Tokenizer modules loaded");
243-
});
244-
245268
// Install React DevTools in development
246269
if (!app.isPackaged && installExtension && REACT_DEVELOPER_TOOLS) {
247270
try {
@@ -255,7 +278,16 @@ if (gotTheLock) {
255278
}
256279

257280
createMenu();
258-
createWindow();
281+
await createWindow();
282+
283+
// Start loading tokenizer modules in background after window is created
284+
// This ensures accurate token counts for first API calls (especially in e2e tests)
285+
// Loading happens asynchronously and won't block the UI
286+
if (loadTokenizerModulesFn) {
287+
void loadTokenizerModulesFn().then(() => {
288+
console.log("Tokenizer modules loaded");
289+
});
290+
}
259291
// No need to auto-start workspaces anymore - they start on demand
260292
});
261293

@@ -269,7 +301,7 @@ if (gotTheLock) {
269301
// Only create window if app is ready and no window exists
270302
// This prevents "Cannot create BrowserWindow before app is ready" error
271303
if (app.isReady() && mainWindow === null) {
272-
createWindow();
304+
void createWindow();
273305
}
274306
});
275307
}

0 commit comments

Comments
 (0)