Back to Home

Electron Guide - Cross-Platform Desktop Apps

Performance Optimization

Electron apps have a reputation for being slow and memory-hungry. It doesn't have to be that way. With the right patterns, your app can feel as snappy as a native app.

The "Electron is slow" meme comes from poorly optimized apps, not Electron itself. VS Code is Electron—and it's fast. The difference is attention to performance: startup time, memory usage, bundle size, and avoiding unnecessary work.

The Four Performance Pillars

Startup Time

Users notice if your app takes more than 2 seconds to show UI. Defer non-critical initialization, use splash screens, lazy load routes.

Memory Usage

Electron apps start at ~150MB. That's fine. But if you leak memory or load huge datasets, users will notice their system slowing down.

Bundle Size

Smaller bundles mean faster downloads and quicker startup. Tree-shake dependencies, lazy load heavy components, compress assets.

Runtime Performance

Smooth 60fps animations, responsive UI, no jank. Avoid blocking the main thread, use requestAnimationFrame, virtualize long lists.

🎯 VibeBlaster Performance Journey

VibeBlaster v1.0 took 8 seconds to start and used 400MB of RAM. After optimization: 2 seconds startup, 180MB RAM. The changes: lazy loading routes, deferring OAuth initialization, virtualizing the post list, and fixing a memory leak in the file watcher.

Profile before optimizing. I spent a week optimizing the wrong things before using DevTools properly.

Startup Time: First Impressions Matter

Users judge your app in the first 2 seconds. Show something fast, even if it's a loading screen. Defer everything that doesn't need to happen immediately.

Show Window Early

Create the window with show: false, then show it as soon as the renderer has content. Don't wait for all initialization.

const win = new BrowserWindow({ 
  show: false  // Hidden initially
});

win.once('ready-to-show', () => {
  win.show();  // Show when content ready
});

Defer Non-Critical Work

Analytics, update checks, and background sync don't need to happen at startup. Use setImmediate orsetTimeout.

// Critical: do immediately
createWindow();

// Non-critical: defer
setImmediate(() => {
  initAnalytics();
  checkForUpdates();
  setupTray();
});

Lazy Load Routes

Don't bundle all your pages into the initial load. Use React.lazy to load routes on demand. The settings page doesn't need to load until the user opens settings.

import { lazy, Suspense } from 'react';

// Load on demand, not at startup
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />  {/* Load immediately */}
        <Route path="/settings" element={<Settings />} />  {/* Lazy */}
        <Route path="/reports" element={<Reports />} />  {/* Lazy */}
      </Routes>
    </Suspense>
  );
}

💡 Splash Screen Pattern

For apps with heavy initialization, show a splash screen immediately while loading. The splash is a simple BrowserWindow with a static HTML file—loads in milliseconds. Close it when the main window is ready. Users perceive the app as faster even if total load time is the same.

Memory Management: Don't Leak

Memory leaks are the silent killer of Electron apps. Your app starts fine, but after hours of use, it's consuming 2GB of RAM. The culprits are almost always event listeners, timers, or references that aren't cleaned up.

Common Memory Leak Sources

  • Event listeners not removed on component unmount
  • Timers/intervals not cleared
  • IPC listeners accumulating over time
  • Closures holding references to large objects
  • DOM nodes detached but still referenced

❌ Leaky Pattern

useEffect(() => {
  // Listener added...
  window.api.onDataUpdate(setData);
  
  // ...but never removed!
}, []);

✅ Clean Pattern

useEffect(() => {
  const cleanup = window.api.onDataUpdate(setData);
  
  // Cleanup on unmount
  return () => cleanup();
}, []);

Virtualize Long Lists

Rendering 10,000 DOM nodes will kill performance. Use virtualization to only render what's visible. Libraries like react-window or react-virtualized handle this.

import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}  // Height of each row
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

// Renders only ~15 visible items, not 10,000

Bundle Size: Ship Less Code

Smaller bundles mean faster startup and smaller downloads. Analyze what you're shipping, remove unused dependencies, and use lighter alternatives.

Analyze Your Bundle

Before optimizing, know what's in your bundle. Use webpack-bundle-analyzer or vite-bundle-visualizer to see what's taking up space.

# Install analyzer
npm install --save-dev webpack-bundle-analyzer

# Add to package.json scripts
"analyze": "webpack --profile --json > stats.json && npx webpack-bundle-analyzer stats.json"

You'll often find surprises: a 500KB library you imported for one function, duplicate dependencies, or dev tools accidentally bundled for production.

Heavy Libraries to Avoid

  • moment.js (232KB) → dayjs (7KB)
  • lodash (70KB) → lodash-es or native
  • axios (14KB) → fetch (built-in)
  • uuid (12KB) → crypto.randomUUID()

Optimization Techniques

  • • Import specific functions, not entire libraries
  • • Use tree-shakeable ES modules
  • • Remove console.logs in production
  • • Compress images, use WebP format

💡 Import Wisely

❌ Imports entire library

import _ from 'lodash';

✅ Tree-shakeable import

import debounce from 'lodash/debounce';

Profiling: Measure, Don't Guess

The biggest performance mistake is optimizing without measuring. Use Chrome DevTools (built into Electron) to find actual bottlenecks, not assumed ones.

DevTools Performance Tab

Open DevTools (Cmd+Option+I), go to Performance tab, click Record, interact with your app, stop recording. The flame graph shows exactly where time is spent.

Look For:

  • • Long tasks (>50ms) blocking the main thread
  • • Excessive re-renders in React components
  • • Layout thrashing (forced reflows)
  • • Memory growing without garbage collection

React DevTools Profiler

  • • Install React DevTools extension
  • • Use Profiler tab to record renders
  • • Find components that render too often
  • • Identify slow render times

⚠️ Profile in Production Mode

Development mode is significantly slower due to React's extra checks, hot reload, and unminified code. Always profile a production build to get accurate numbers. What's slow in dev might be fine in prod—and vice versa.

Performance Best Practices

Profile before optimizing—measure, don't guess
Show UI fast, defer non-critical initialization
Lazy load routes and heavy components
Clean up listeners, timers, and subscriptions
Virtualize long lists (react-window)
Analyze and minimize bundle size
Use lighter library alternatives
Test performance on low-end hardware

You've Completed the Guide! 🎉

You now have a complete playbook for building production Electron apps: architecture, UI, native integration, security, auto-updates, testing, packaging, and performance. These aren't theoretical patterns—they're battle-tested approaches from shipping real apps.

The key insight: Electron apps can be fast, secure, and professional. The "Electron is slow" meme comes from apps that ignore these patterns. Now you know better.

Next step: Build something! The best way to learn is to apply these patterns to a real project. Start small, ship early, iterate based on what you learn.