Optimize window startup and session configuration
Improves perceived performance by creating the window hidden, showing it after first paint, and maximizing on Windows after display. Defers non-critical tasks and session configuration to avoid blocking UI initialization. Adds high-resolution startup timing markers and refines session/user agent handling for OAuth compatibility.
This commit is contained in:
@@ -28,13 +28,21 @@ ipcMain.removeHandler('window-close');
|
|||||||
|
|
||||||
|
|
||||||
function createWindow(startUrl) {
|
function createWindow(startUrl) {
|
||||||
// Get the available screen size
|
// Capture high‑resolution startup timing markers
|
||||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
const perfMarks = { createWindow_called: performance.now() };
|
||||||
|
|
||||||
// Ensure nativeWindowOpen is disabled
|
// Get the available screen size (avoid full workArea allocation jank by starting slightly smaller then maximizing later if desired)
|
||||||
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
||||||
|
const initialWidth = Math.min(width, Math.round(width * 0.9));
|
||||||
|
const initialHeight = Math.min(height, Math.round(height * 0.9));
|
||||||
|
|
||||||
|
// Window is created hidden; we only show after first meaningful paint to avoid OS‑level pointer jank while Chromium initializes
|
||||||
let windowOptions = {
|
let windowOptions = {
|
||||||
width,
|
width: initialWidth,
|
||||||
height,
|
height: initialHeight,
|
||||||
|
show: false,
|
||||||
|
useContentSize: true,
|
||||||
|
backgroundColor: '#121212', // avoids white flash & early extra paints
|
||||||
resizable: true,
|
resizable: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
@@ -44,23 +52,24 @@ function createWindow(startUrl) {
|
|||||||
enableRemoteModule: false, // Deprecated and slow
|
enableRemoteModule: false, // Deprecated and slow
|
||||||
nodeIntegrationInSubFrames: false, // Security & performance
|
nodeIntegrationInSubFrames: false, // Security & performance
|
||||||
nativeWindowOpen: false,
|
nativeWindowOpen: false,
|
||||||
spellcheck: false, // Disable if not needed
|
spellcheck: false,
|
||||||
webSecurity: true,
|
webSecurity: true,
|
||||||
allowRunningInsecureContent: false,
|
allowRunningInsecureContent: false,
|
||||||
experimentalFeatures: false,
|
experimentalFeatures: false,
|
||||||
offscreen: false, // Ensure on-screen rendering for GPU
|
offscreen: false,
|
||||||
enableWebSQL: false, // Disable deprecated features
|
enableWebSQL: false,
|
||||||
plugins: false, // Disable plugins that might interfere with GPU
|
plugins: false,
|
||||||
|
backgroundThrottling: false, // keep UI responsive during early load
|
||||||
// OAuth compatibility settings
|
// OAuth compatibility settings
|
||||||
partition: 'persist:main',
|
partition: 'persist:main',
|
||||||
sandbox: false // Allow full browser capabilities for OAuth
|
sandbox: false
|
||||||
},
|
},
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
icon: process.platform === 'darwin'
|
icon: process.platform === 'darwin'
|
||||||
? path.join(__dirname, 'assets/images/Logos/Nebula-Favicon.icns')
|
? path.join(__dirname, 'assets/images/Logos/Nebula-Favicon.icns')
|
||||||
: path.join(__dirname, 'assets/images/Logos/Nebula-favicon.png'),
|
: path.join(__dirname, 'assets/images/Logos/Nebula-favicon.png'),
|
||||||
title: 'Nebula',
|
title: 'Nebula'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
@@ -81,6 +90,7 @@ function createWindow(startUrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const win = new BrowserWindow(windowOptions);
|
const win = new BrowserWindow(windowOptions);
|
||||||
|
perfMarks.browserWindow_instantiated = performance.now();
|
||||||
|
|
||||||
// Allow window.open() popups (e.g. OAuth / SSO / school portals) so that
|
// Allow window.open() popups (e.g. OAuth / SSO / school portals) so that
|
||||||
// POST form submissions and opener relationships are preserved.
|
// POST form submissions and opener relationships are preserved.
|
||||||
@@ -114,6 +124,7 @@ function createWindow(startUrl) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
win.loadFile('renderer/index.html');
|
win.loadFile('renderer/index.html');
|
||||||
|
perfMarks.loadFile_issued = performance.now();
|
||||||
|
|
||||||
// if caller passed in a URL, forward it to the renderer after load
|
// if caller passed in a URL, forward it to the renderer after load
|
||||||
if (startUrl) {
|
if (startUrl) {
|
||||||
@@ -125,50 +136,64 @@ function createWindow(startUrl) {
|
|||||||
// Set default zoom to 100%
|
// Set default zoom to 100%
|
||||||
const zoomFactor = 1.0;
|
const zoomFactor = 1.0;
|
||||||
const loadStartTime = Date.now();
|
const loadStartTime = Date.now();
|
||||||
|
// Show window ASAP after first paint for perceived performance
|
||||||
|
let shown = false;
|
||||||
|
const showNow = (reason) => {
|
||||||
|
if (shown) return;
|
||||||
|
shown = true;
|
||||||
|
win.show();
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Defer maximize to next frame to avoid large-surface first paint cost
|
||||||
|
setTimeout(() => {
|
||||||
|
try { win.maximize(); } catch {}
|
||||||
|
}, 16);
|
||||||
|
}
|
||||||
|
console.log(`[Startup] Window shown (${reason}) in ${(performance.now() - perfMarks.createWindow_called).toFixed(1)}ms`);
|
||||||
|
};
|
||||||
|
|
||||||
|
win.webContents.once('ready-to-show', () => showNow('ready-to-show'));
|
||||||
|
// Fallback in case ready-to-show is delayed
|
||||||
|
setTimeout(() => showNow('timeout-fallback'), 4000);
|
||||||
|
|
||||||
win.webContents.on('did-finish-load', () => {
|
win.webContents.on('did-finish-load', () => {
|
||||||
win.webContents.setZoomFactor(zoomFactor);
|
win.webContents.setZoomFactor(zoomFactor);
|
||||||
|
|
||||||
// Track load time for performance monitoring
|
|
||||||
const loadTime = Date.now() - loadStartTime;
|
const loadTime = Date.now() - loadStartTime;
|
||||||
perfMonitor.trackLoadTime(win.webContents.getURL(), loadTime);
|
perfMonitor.trackLoadTime(win.webContents.getURL(), loadTime);
|
||||||
|
perfMarks.did_finish_load = performance.now();
|
||||||
|
|
||||||
|
// Defer heavier, non‑critical tasks to next idle slice to keep UI smooth
|
||||||
|
setTimeout(() => {
|
||||||
|
// Kick off GPU status check here (was earlier) to avoid competing with first paint
|
||||||
|
gpuConfig.checkGPUStatus()
|
||||||
|
.then(gpuStatus => {
|
||||||
|
console.log('[Deferred] GPU Configuration Results:');
|
||||||
|
console.log('- GPU Status:', gpuStatus);
|
||||||
|
console.log('- Recommendations:', gpuConfig.getRecommendations());
|
||||||
|
})
|
||||||
|
.catch(err => console.error('[Deferred] GPU status check failed:', err));
|
||||||
|
|
||||||
|
// Start performance monitoring after initial load
|
||||||
|
perfMonitor.start();
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Renderer manages history; no main-process recording here
|
// Renderer manages history; no main-process recording here
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished initialization
|
// This method will be called when Electron has finished initialization
|
||||||
app.whenReady().then(async () => {
|
// Configure sessions asynchronously (non-blocking for window creation)
|
||||||
// Check GPU status and handle errors
|
function configureSessionsAsync() {
|
||||||
const gpuStatus = await gpuConfig.checkGPUStatus();
|
|
||||||
console.log('GPU Configuration Results:');
|
|
||||||
console.log('- GPU Status:', gpuStatus);
|
|
||||||
console.log('- Recommendations:', gpuConfig.getRecommendations());
|
|
||||||
|
|
||||||
// Handle GPU process crashes
|
|
||||||
app.on('gpu-process-crashed', (event, killed) => {
|
|
||||||
console.warn('GPU process crashed, killed:', killed);
|
|
||||||
if (!killed) {
|
|
||||||
console.log('Attempting to recover GPU process...');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optimize session settings for performance and OAuth compatibility
|
|
||||||
const sessionsToConfigure = [session.fromPartition('persist:main'), session.defaultSession];
|
const sessionsToConfigure = [session.fromPartition('persist:main'), session.defaultSession];
|
||||||
try {
|
try {
|
||||||
for (const ses of sessionsToConfigure) {
|
for (const ses of sessionsToConfigure) {
|
||||||
// Configure session for OAuth compatibility (Google, etc.)
|
if (!ses) continue;
|
||||||
ses.setPermissionRequestHandler((webContents, permission, callback) => {
|
ses.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||||
// Allow necessary permissions for OAuth flows
|
|
||||||
if (['notifications', 'geolocation', 'camera', 'microphone'].includes(permission)) {
|
if (['notifications', 'geolocation', 'camera', 'microphone'].includes(permission)) {
|
||||||
callback(false); // Deny by default for privacy
|
callback(false);
|
||||||
} else {
|
} else {
|
||||||
callback(true); // Allow others like storage access
|
callback(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure user agent for better compatibility:
|
|
||||||
// Previously this forced Chrome/120 which is stale and can cause anti-bot / Cloudflare challenges
|
|
||||||
// to fail due to UA / feature mismatch fingerprinting. Use the real Chromium UA then append branding.
|
|
||||||
try {
|
try {
|
||||||
const realUA = ses.getUserAgent();
|
const realUA = ses.getUserAgent();
|
||||||
if (realUA && !realUA.includes('Nebula/')) {
|
if (realUA && !realUA.includes('Nebula/')) {
|
||||||
@@ -177,24 +202,17 @@ app.whenReady().then(async () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to read real user agent, keeping default:', e);
|
console.warn('Failed to read real user agent, keeping default:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure cookies for OAuth compatibility
|
|
||||||
ses.cookies.on('changed', (event, cookie, cause, removed) => {
|
ses.cookies.on('changed', (event, cookie, cause, removed) => {
|
||||||
// Log cookie changes for debugging OAuth issues
|
|
||||||
if (cookie.domain && (cookie.domain.includes('google') || cookie.domain.includes('accounts'))) {
|
if (cookie.domain && (cookie.domain.includes('google') || cookie.domain.includes('accounts'))) {
|
||||||
console.log(`Cookie ${removed ? 'removed' : 'added'}: ${cookie.name} for ${cookie.domain}`);
|
console.log(`Cookie ${removed ? 'removed' : 'added'}: ${cookie.name} for ${cookie.domain}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optional: add headers only for OAuth flows; avoid forcing cache headers globally
|
|
||||||
ses.webRequest.onBeforeSendHeaders((details, callback) => {
|
ses.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||||
const headers = details.requestHeaders;
|
const headers = details.requestHeaders;
|
||||||
// Add richer headers for sensitive flows (OAuth / login) to look like a real browser
|
|
||||||
if (details.url.includes('accounts.google.com') || details.url.includes('oauth')) {
|
if (details.url.includes('accounts.google.com') || details.url.includes('oauth')) {
|
||||||
headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
|
headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
|
||||||
headers['Accept'] = headers['Accept'] || 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8';
|
headers['Accept'] = headers['Accept'] || 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8';
|
||||||
}
|
}
|
||||||
// Ensure Accept-Language is present (Cloudflare & some WAFs use this in heuristics)
|
|
||||||
if (!headers['Accept-Language'] && !headers['accept-language']) {
|
if (!headers['Accept-Language'] && !headers['accept-language']) {
|
||||||
headers['Accept-Language'] = 'en-US,en;q=0.9';
|
headers['Accept-Language'] = 'en-US,en;q=0.9';
|
||||||
}
|
}
|
||||||
@@ -205,13 +223,25 @@ app.whenReady().then(async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Session setup error:', err);
|
console.error('Session setup error:', err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start performance monitoring
|
app.whenReady().then(() => {
|
||||||
perfMonitor.start();
|
const t0 = performance.now();
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
console.log('[Startup] createWindow invoked in', (performance.now() - t0).toFixed(1), 'ms after app.whenReady');
|
||||||
|
|
||||||
|
// Handle GPU process crashes (still register early)
|
||||||
|
app.on('gpu-process-crashed', (event, killed) => {
|
||||||
|
console.warn('GPU process crashed, killed:', killed);
|
||||||
|
if (!killed) {
|
||||||
|
console.log('Attempting to recover GPU process...');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Defer session configuration to microtask/next tick (already inexpensive) – keep explicit
|
||||||
|
setImmediate(configureSessionsAsync);
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
// Set macOS dock icon using an icns file for proper display.
|
|
||||||
app.dock.setIcon(path.join(__dirname, 'assets/images/Logos/Nebula-Icon.icns'));
|
app.dock.setIcon(path.join(__dirname, 'assets/images/Logos/Nebula-Icon.icns'));
|
||||||
}
|
}
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user