From 0315155e4f33f04b74597f0065107393f06cfd4d Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Sat, 5 Sep 2026 16:15:37 +1000
Subject: [PATCH] perf: optimize application startup
---
electron-builder.json5 | 10 +-
electron/main.ts | 46 ++++------
electron/rendererServer.ts | 11 ++-
electron/updater.ts | 11 ++-
electron/windows.ts | 3 +
src/App.tsx | 92 +++++++++----------
src/components/launch/HudWindow.tsx | 11 +++
src/components/launch/LaunchWindow.tsx | 2 +-
src/components/launch/MarqueeText.tsx | 37 ++++++++
src/components/launch/SourceSelector.tsx | 39 +-------
.../launch/popovers/ProjectPopover.tsx | 33 ++++---
src/components/video-editor/EditorWindow.tsx | 26 ++++++
src/components/video-editor/SettingsPanel.tsx | 8 +-
.../video-editor/export/useExportRunner.ts | 20 ++--
.../project/useProjectLibraryController.ts | 2 +-
.../video-editor/projectPersistence.ts | 2 +-
vite.config.ts | 1 -
17 files changed, 205 insertions(+), 149 deletions(-)
create mode 100644 src/components/launch/HudWindow.tsx
create mode 100644 src/components/launch/MarqueeText.tsx
create mode 100644 src/components/video-editor/EditorWindow.tsx
diff --git a/electron-builder.json5 b/electron-builder.json5
index a333a872..197dcc17 100644
--- a/electron-builder.json5
+++ b/electron-builder.json5
@@ -8,7 +8,7 @@
"node_modules/ffmpeg-static/**",
"node_modules/ffprobe-static/**",
"node_modules/uiohook-napi/**",
- "electron/native/**"
+ "electron/native/bin/**"
],
"productName": "Recordly",
"npmRebuild": true,
@@ -19,8 +19,13 @@
},
"files": [
"dist",
+ "!dist/wallpapers/**",
"dist-electron",
- "electron/native",
+ "electron/native/bin",
+ "!electron/native/bin/**/whisper-bench*",
+ "!electron/native/bin/**/whisper-quantize*",
+ "!electron/native/bin/**/whisper-server*",
+ "!electron/native/bin/**/whisper-vad-speech-segments*",
"!node_modules/ffprobe-static/bin/darwin/**",
"!node_modules/ffprobe-static/bin/linux/**",
"!node_modules/ffprobe-static/bin/win32/ia32/**",
@@ -111,4 +116,3 @@
"artifactName": "${productName}-windows-${arch}.${ext}"
}
}
-
diff --git a/electron/main.ts b/electron/main.ts
index 1ea7dce6..89072667 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -935,17 +935,11 @@ app.whenReady().then(async () => {
// Recordly does not use WebHID, Web Serial, or WebUSB. Do not grant devices by default.
session.defaultSession.setDevicePermissionHandler(() => false);
- if (process.platform === "darwin") {
- const cameraStatus = systemPreferences.getMediaAccessStatus("camera");
- if (cameraStatus !== "granted") {
- await systemPreferences.askForMediaAccess("camera");
- }
-
- const micStatus = systemPreferences.getMediaAccessStatus("microphone");
- if (micStatus !== "granted") {
- await systemPreferences.askForMediaAccess("microphone");
- }
- } else if (process.platform === "win32") {
+ // macOS prompts for camera and microphone access at the point of use. Asking
+ // here blocks the first window behind two modal OS permission flows and makes
+ // a fresh install look hung. Windows has no equivalent request API, so retain
+ // its diagnostic warnings.
+ if (process.platform === "win32") {
const cameraStatus = systemPreferences.getMediaAccessStatus("camera");
const micStatus = systemPreferences.getMediaAccessStatus("microphone");
if (cameraStatus !== "granted") {
@@ -986,22 +980,20 @@ app.whenReady().then(async () => {
updateTrayMenu();
}
setupApplicationMenu();
- // Ensure recordings directory exists
- await ensureRecordingsDir();
-
- if (!VITE_DEV_SERVER_URL) {
- try {
- await ensurePackagedRendererServer(RENDERER_DIST);
- } catch (error) {
- console.warn("[renderer-server] Failed to start packaged renderer server:", error);
- }
- }
-
- try {
- await ensureMediaServer();
- } catch (error) {
- console.warn("[media-server] Failed to start media server:", error);
- }
+ await Promise.all([
+ ensureRecordingsDir(),
+ !VITE_DEV_SERVER_URL
+ ? ensurePackagedRendererServer(RENDERER_DIST).catch((error) => {
+ console.warn(
+ "[renderer-server] Failed to start packaged renderer server:",
+ error,
+ );
+ })
+ : Promise.resolve(),
+ ensureMediaServer().catch((error) => {
+ console.warn("[media-server] Failed to start media server:", error);
+ }),
+ ]);
registerIpcHandlers(
createEditorWindowWrapper,
diff --git a/electron/rendererServer.ts b/electron/rendererServer.ts
index 8f0d5662..f3453a06 100644
--- a/electron/rendererServer.ts
+++ b/electron/rendererServer.ts
@@ -28,6 +28,15 @@ function getContentType(filePath: string): string {
return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
}
+function getCacheControl(filePath: string): string {
+ // Vite fingerprints production assets, so they can be reused across the HUD,
+ // picker, toast, and editor windows without revalidation. Keep index.html
+ // uncached because it points at the current fingerprinted files.
+ return /-[A-Za-z0-9_-]{8,}\.[^.]+$/.test(path.basename(filePath))
+ ? "public, max-age=31536000, immutable"
+ : "no-cache";
+}
+
function resolveRequestedFilePath(rootDir: string, requestPathname: string): string | null {
const trimmedPathname = requestPathname === "/" ? "/index.html" : requestPathname;
@@ -72,7 +81,7 @@ async function servePackagedRendererRequest(
const fileContents = await fs.readFile(filePath);
response.writeHead(200, {
- "Cache-Control": "no-cache",
+ "Cache-Control": getCacheControl(filePath),
"Content-Type": getContentType(filePath),
});
diff --git a/electron/updater.ts b/electron/updater.ts
index 86c2d76d..b9c4e5dc 100644
--- a/electron/updater.ts
+++ b/electron/updater.ts
@@ -8,6 +8,7 @@ import { readAppSetting, writeAppSetting } from "./appSettingsStore";
import { EXPERIMENTAL_UPDATE_DESCRIPTION, getUpdateChannelConfiguration } from "./updateChannel";
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
+const INITIAL_UPDATE_CHECK_DELAY_MS = 15 * 1000;
export const UPDATE_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000;
const DISMISSED_READY_REMINDER_DELAY_MS = 5 * 60 * 1000;
const AUTO_UPDATES_DISABLED = process.env.RECORDLY_DISABLE_AUTO_UPDATES === "1";
@@ -71,6 +72,7 @@ let updaterInitialized = false;
let updateCheckInProgress = false;
let manualCheckRequested = false;
let periodicCheckTimer: NodeJS.Timeout | null = null;
+let initialCheckTimer: NodeJS.Timeout | null = null;
let deferredReminderTimer: NodeJS.Timeout | null = null;
let devPreviewProgressTimer: NodeJS.Timeout | null = null;
let currentToastPayload: UpdateToastPayload | null = null;
@@ -905,7 +907,10 @@ export function setupAutoUpdates(
void showDownloadedUpdateDialog(getMainWindow, info.version);
});
- void checkForAppUpdates(getMainWindow);
+ initialCheckTimer = setTimeout(() => {
+ initialCheckTimer = null;
+ void checkForAppUpdates(getMainWindow);
+ }, INITIAL_UPDATE_CHECK_DELAY_MS);
periodicCheckTimer = setInterval(() => {
void checkForAppUpdates(getMainWindow);
}, UPDATE_CHECK_INTERVAL_MS);
@@ -913,6 +918,10 @@ export function setupAutoUpdates(
app.on("before-quit", () => {
clearDeferredReminderTimer();
clearDevPreviewProgressTimer();
+ if (initialCheckTimer) {
+ clearTimeout(initialCheckTimer);
+ initialCheckTimer = null;
+ }
if (periodicCheckTimer) {
clearInterval(periodicCheckTimer);
periodicCheckTimer = null;
diff --git a/electron/windows.ts b/electron/windows.ts
index eb17b2e2..23e874fa 100644
--- a/electron/windows.ts
+++ b/electron/windows.ts
@@ -447,6 +447,7 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean)
});
export function createHudOverlayWindow(): BrowserWindow {
+ const perfStart = Date.now();
loadHudOverlayCaptureProtectionSetting();
hudOverlayFallbackExpanded = false;
hudOverlayWebcamPreviewVisible = false;
@@ -557,6 +558,7 @@ export function createHudOverlayWindow(): BrowserWindow {
}
win.webContents.on("did-finish-load", () => {
+ console.log(`[PERF:MAIN] HUD Window: did-finish-load in ${Date.now() - perfStart}ms`);
win?.webContents.send("main-process-message", new Date().toLocaleString());
// Safety fallback if renderer-ready signal never arrives.
setTimeout(() => {
@@ -577,6 +579,7 @@ export function createHudOverlayWindow(): BrowserWindow {
const handleHudRendererReady = () => {
if (!win.isDestroyed()) {
+ console.log(`[PERF:MAIN] HUD Window: renderer-ready in ${Date.now() - perfStart}ms`);
showHudWindow();
}
};
diff --git a/src/App.tsx b/src/App.tsx
index 6970efb1..25401793 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,54 +1,56 @@
-import { useEffect, useState } from "react";
-import { AnnouncementDialog } from "./components/announcements/AnnouncementDialog";
-import { LiveAnnouncementNotifications } from "./components/announcements/LiveAnnouncementNotifications";
-import { CountdownOverlay } from "./components/countdown/CountdownOverlay";
-import { LaunchWindow } from "./components/launch/LaunchWindow";
-import { SourceSelector } from "./components/launch/SourceSelector";
-import { UpdateToastWindow } from "./components/launch/UpdateToastWindow";
-import { Toaster } from "./components/ui/sonner";
-import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog";
-import VideoEditor from "./components/video-editor/VideoEditor";
+import { lazy, Suspense, useEffect, useState } from "react";
import { useI18n } from "./contexts/I18nContext";
-import { ShortcutsProvider } from "./contexts/ShortcutsContext";
-import { loadAllCustomFonts } from "./lib/customFonts";
+
+const HudWindow = lazy(() => import("./components/launch/HudWindow"));
+const SourceSelector = lazy(() =>
+ import("./components/launch/SourceSelector").then((module) => ({
+ default: module.SourceSelector,
+ })),
+);
+const CountdownOverlay = lazy(() =>
+ import("./components/countdown/CountdownOverlay").then((module) => ({
+ default: module.CountdownOverlay,
+ })),
+);
+const UpdateToastWindow = lazy(() =>
+ import("./components/launch/UpdateToastWindow").then((module) => ({
+ default: module.UpdateToastWindow,
+ })),
+);
+const EditorWindow = lazy(() => import("./components/video-editor/EditorWindow"));
export default function App() {
- const [windowType, setWindowType] = useState("");
+ const [windowType] = useState(
+ () => new URLSearchParams(window.location.search).get("windowType") || "",
+ );
const { t } = useI18n();
const appIconSrc = "/app-icons/recordly-128.png";
useEffect(() => {
- const params = new URLSearchParams(window.location.search);
- const type = params.get("windowType") || "";
- setWindowType(type);
- document.documentElement.dataset.windowType = type;
+ document.documentElement.dataset.windowType = windowType;
if (
- type === "hud-overlay" ||
- type === "source-selector" ||
- type === "countdown" ||
- type === "update-toast"
+ windowType === "hud-overlay" ||
+ windowType === "source-selector" ||
+ windowType === "countdown" ||
+ windowType === "update-toast"
) {
document.body.style.background = "transparent";
document.documentElement.style.background = "transparent";
document.getElementById("root")?.style.setProperty("background", "transparent");
}
- if (type === "hud-overlay") {
+ if (windowType === "hud-overlay") {
document.documentElement.classList.add("hud-overlay-window");
document.body.classList.add("hud-overlay-window");
document.getElementById("root")?.classList.add("hud-overlay-window");
window.electronAPI?.hudOverlaySetIgnoreMouse?.(true);
- } else if (type === "update-toast") {
+ } else if (windowType === "update-toast") {
document.documentElement.style.overflow = "visible";
document.body.style.overflow = "visible";
document.getElementById("root")?.style.setProperty("overflow", "visible");
}
-
- loadAllCustomFonts().catch((error) => {
- console.error("Failed to load custom fonts:", error);
- });
- }, []);
+ }, [windowType]);
useEffect(() => {
document.title =
@@ -57,33 +59,25 @@ export default function App() {
: t("app.name", "Recordly");
}, [windowType, t]);
+ let content;
switch (windowType) {
case "hud-overlay":
- return (
- <>
-