perf(electron): defer hidden-start renderer creation (#10327)

Merged — locally validated (6/6 focused lazy-window tests, plus the wider electron suite, typecheck:core clean, gates green). Thanks!
This commit is contained in:
backryun
2026-08-20 23:32:08 +09:00
committed by GitHub
parent a352c23bad
commit 5e508147c4
4 changed files with 170 additions and 32 deletions

View File

@@ -0,0 +1,28 @@
/** Pure helpers for deciding and driving the Electron dashboard window lifecycle. */
function shouldStartHidden({ argv = [], loginItemSettings = {} } = {}) {
return (
argv.includes("--hidden") ||
argv.includes("--minimized") ||
loginItemSettings.wasOpenedAsHidden === true
);
}
function showOrCreateWindow({ appReady, getWindow, createWindow }) {
if (!appReady) return null;
const currentWindow = getWindow();
if (!currentWindow || currentWindow.isDestroyed()) {
return createWindow();
}
if (currentWindow.isMinimized()) currentWindow.restore();
currentWindow.show();
currentWindow.focus();
return currentWindow;
}
module.exports = {
shouldStartHidden,
showOrCreateWindow,
};

View File

@@ -40,6 +40,7 @@ const { resolveDarwinHelperExecutable } = require("./lib/resolveNodeHelper");
const { resolveRemoteServerUrl, isValidHttpUrl } = require("./lib/resolveRemoteServerUrl");
const { writeRemoteServerUrl } = require("./lib/remoteServerPreferences");
const { buildReadinessUrl, waitForServer } = require("./lib/serverReadiness");
const { shouldStartHidden, showOrCreateWindow } = require("./lib/windowLifecycle");
// ── Single Instance Lock ───────────────────────────────────
const gotTheLock = app.requestSingleInstanceLock();
@@ -49,11 +50,7 @@ if (!gotTheLock) {
}
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
showMainWindow();
});
// ── Environment Detection ──────────────────────────────────
@@ -71,6 +68,7 @@ let nextServer = null;
let serverPort = 20128;
let isServerStopped = false;
let remoteServerPromptWindow = null;
let keepAliveWithoutWindows = false;
// ── Remote Server Mode ──────────────────────────────────────
// Lets the desktop shell attach to an already-running OmniRoute server (e.g. a
@@ -366,6 +364,8 @@ function setupContentSecurityPolicy() {
// ── Create Window ──────────────────────────────────────────
function createWindow() {
if (mainWindow && !mainWindow.isDestroyed()) return mainWindow;
// Platform-conditional options (#9)
const platformWindowOptions =
process.platform === "darwin"
@@ -397,16 +397,10 @@ function createWindow() {
mainWindow.webContents.openDevTools({ mode: "detach" });
}
// Show window when ready (unless starting minimized/hidden in tray)
// Hidden startup skips createWindow() entirely; any created dashboard is explicit.
mainWindow.once("ready-to-show", () => {
const startHidden =
process.argv.includes("--hidden") ||
process.argv.includes("--minimized") ||
app.getLoginItemSettings().wasOpenedAsHidden;
if (!startHidden) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show();
} else {
console.log("[Electron] Launched hidden in background tray");
}
});
@@ -437,6 +431,16 @@ function createWindow() {
mainWindow.on("closed", () => {
mainWindow = null;
});
return mainWindow;
}
function showMainWindow() {
return showOrCreateWindow({
appReady: app.isReady(),
getWindow: () => mainWindow,
createWindow,
});
}
// ── System Tray ────────────────────────────────────────────
@@ -465,12 +469,7 @@ function createTray() {
const contextMenu = Menu.buildFromTemplate([
{
label: "Open OmniRoute",
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
},
click: () => showMainWindow(),
},
{
label: "Open Dashboard",
@@ -523,10 +522,7 @@ function createTray() {
tray.setContextMenu(contextMenu);
tray.on("double-click", () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
showMainWindow();
});
}
@@ -1094,9 +1090,20 @@ app.whenReady().then(async () => {
process.argv.includes("--headless") ||
process.argv.includes("--cli") ||
process.env.OMNIROUTE_HEADLESS === "true";
const startHidden =
!isHeadless &&
shouldStartHidden({
argv: process.argv,
loginItemSettings: app.getLoginItemSettings(),
});
keepAliveWithoutWindows = startHidden;
// Fix #1: Start server and WAIT for readiness before showing window
startNextServer();
if (!isHeadless) {
createTray();
}
let serverReady = true;
if (!isDev) {
// Probe the lightweight auth-exempt endpoint instead of aggregating full monitoring state.
@@ -1105,9 +1112,10 @@ app.whenReady().then(async () => {
if (isHeadless) {
console.log("[Electron] Headless mode active — UI window and tray icon skipped");
} else if (startHidden) {
console.log("[Electron] Launched hidden in background tray without a renderer");
} else {
createWindow();
createTray();
showMainWindow();
}
setupIpcHandlers();
@@ -1115,7 +1123,7 @@ app.whenReady().then(async () => {
// If readiness timed out (e.g. very long first-launch migrations), don't leave the
// window stuck on a hanging connection — keep polling and reload once it responds (#2460).
if (!isDev && !serverReady && !isHeadless) {
if (!isDev && !serverReady && !isHeadless && !startHidden) {
void waitForServer(getServerReadinessUrl(), 300000).then((ready) => {
if (ready && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.loadURL(getServerUrl());
@@ -1133,11 +1141,7 @@ app.whenReady().then(async () => {
// macOS: recreate window when dock icon clicked
app.on("activate", () => {
if (isHeadless) return;
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
} else if (mainWindow) {
mainWindow.show();
}
showMainWindow();
});
});
@@ -1147,7 +1151,7 @@ app.on("window-all-closed", () => {
process.argv.includes("--headless") ||
process.argv.includes("--cli") ||
process.env.OMNIROUTE_HEADLESS === "true";
if (process.platform !== "darwin" && !isHeadless) {
if (process.platform !== "darwin" && !isHeadless && !keepAliveWithoutWindows) {
app.quit();
}
});

View File

@@ -64,6 +64,7 @@
"remoteServerPromptRenderer.js",
"lib/resolveServerEntry.js",
"lib/resolveNodeHelper.js",
"lib/windowLifecycle.js",
"lib/resolveRemoteServerUrl.js",
"lib/remoteServerPreferences.js",
"lib/serverReadiness.js",

View File

@@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { createRequire } from "node:module";
import { describe, it } from "node:test";
const require = createRequire(import.meta.url);
const { shouldStartHidden, showOrCreateWindow } = require("../../electron/lib/windowLifecycle");
describe("Electron hidden-start window lifecycle", () => {
it("detects explicit hidden flags and OS login-item hidden launches", () => {
assert.equal(shouldStartHidden({ argv: ["electron", "--hidden"] }), true);
assert.equal(shouldStartHidden({ argv: ["electron", "--minimized"] }), true);
assert.equal(
shouldStartHidden({ argv: ["electron"], loginItemSettings: { wasOpenedAsHidden: true } }),
true
);
assert.equal(shouldStartHidden({ argv: ["electron"], loginItemSettings: {} }), false);
});
it("creates the dashboard only when an explicit open action has no live window", () => {
const createdWindow = { id: "created" };
let createCalls = 0;
const result = showOrCreateWindow({
appReady: true,
getWindow: () => null,
createWindow: () => {
createCalls += 1;
return createdWindow;
},
});
assert.equal(result, createdWindow);
assert.equal(createCalls, 1);
});
it("restores, shows, and focuses an existing dashboard without recreating it", () => {
const calls: string[] = [];
const existingWindow = {
isDestroyed: () => false,
isMinimized: () => true,
restore: () => calls.push("restore"),
show: () => calls.push("show"),
focus: () => calls.push("focus"),
};
const result = showOrCreateWindow({
appReady: true,
getWindow: () => existingWindow,
createWindow: () => {
throw new Error("must not recreate a live dashboard");
},
});
assert.equal(result, existingWindow);
assert.deepEqual(calls, ["restore", "show", "focus"]);
});
it("does not create a BrowserWindow before Electron is ready", () => {
let createCalls = 0;
const result = showOrCreateWindow({
appReady: false,
getWindow: () => null,
createWindow: () => {
createCalls += 1;
},
});
assert.equal(result, null);
assert.equal(createCalls, 0);
});
it("routes tray, second-instance, and macOS activation opens through the lazy helper", () => {
const mainSource = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8");
assert.match(mainSource, /app\.on\("second-instance", \(\) => \{\s*showMainWindow\(\);/);
assert.match(mainSource, /label: "Open OmniRoute",\s*click: \(\) => showMainWindow\(\)/);
assert.match(mainSource, /tray\.on\("double-click", \(\) => \{\s*showMainWindow\(\);/);
assert.match(mainSource, /app\.on\("activate", \(\) => \{[\s\S]*?showMainWindow\(\);/);
});
it("keeps hidden startup renderer-free until an explicit open action", () => {
const mainSource = readFileSync(join(import.meta.dirname, "../../electron/main.js"), "utf8");
const readyBlock = mainSource.slice(mainSource.indexOf("app.whenReady().then"));
assert.match(
readyBlock,
/startNextServer\(\);\s*if \(!isHeadless\) \{\s*createTray\(\);\s*\}/,
"the server and tray must start before the hidden/visible renderer decision"
);
assert.match(
readyBlock,
/if \(isHeadless\)[\s\S]*?else if \(startHidden\)[\s\S]*?else \{\s*showMainWindow\(\);/
);
assert.doesNotMatch(
readyBlock.match(/else if \(startHidden\)[\s\S]*?\} else \{/s)?.[0] ?? "",
/createWindow\(|showMainWindow\(/
);
assert.match(
readyBlock,
/\}\s*setupIpcHandlers\(\);\s*setupAutoUpdater\(\);/,
"IPC and updater setup must remain active when no renderer was created"
);
});
});