All skills
jwynia avatar

/electron-best-practices

@075cb8f
by J Wyniajwynia/agent-skills160 stars
20

Guide AI agents through Electron app development with React including security patterns, type-safe IPC, React integration, packaging with code signing, and testing. Keywords: electron, electron-vite, electron-forge, contextBridge, IPC, security, react, packaging, code signing, notarization, playwright, desktop app.

Use this Skill: https://skilld.dev/gh/jwynia/agent-skills/electron-best-practices

This session only. Nothing lands on disk.

referencespackagingbundle-optimization.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Bundle Size Optimization and Performance

Overview

An unoptimized Electron app easily ships at 120-150 MB or more. With careful attention to bundling, tree shaking, asset optimization, and native module handling, you can reduce this to 45-60 MB. This matters for download times, disk usage, and differential update size.


Size Budget

Component Unoptimized Target Notes
Electron binary ~70 MB ~70 MB Fixed cost, cannot reduce
App code (main) 5-15 MB 1-3 MB Tree shaking, minification
App code (renderer) 10-30 MB 3-8 MB Code splitting, lazy loading
Node modules 30-50 MB 5-15 MB Prune devDeps, externalize natives
Assets 10-30 MB 5-10 MB Compress images, subset fonts

Build Configuration with electron-vite

// electron.vite.config.ts
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  main: {
    plugins: [externalizeDepsPlugin()],
    build: {
      rollupOptions: {
        external: ['better-sqlite3', 'sharp'], // Native modules
      },
      minify: 'terser',
      terserOptions: {
        compress: { drop_console: true, drop_debugger: true, passes: 2 },
      },
      sourcemap: false,
    },
  },
  preload: {
    plugins: [externalizeDepsPlugin()],
    build: {
      minify: 'terser',
      sourcemap: false,
      rollupOptions: {
        output: { inlineDynamicImports: true }, // Single file, no splitting
      },
    },
  },
  renderer: {
    plugins: [react()],
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
            ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
          },
        },
      },
      sourcemap: false,
      minify: 'terser',
      chunkSizeWarningLimit: 500,
    },
  },
});

Source Map Strategy

Use 'source-map' in development, false or 'hidden' in production. If you use Sentry or Bugsnag, upload maps during build then delete them from the bundle:

npx sentry-cli sourcemaps upload --release=$VERSION ./out/renderer
rm -rf ./out/renderer/**/*.map

Tree Shaking

// BAD - Imports entire library
import _ from 'lodash';

// GOOD - Named import from subpath
import groupBy from 'lodash/groupBy';

// BEST - ES module version for full tree shaking
import { groupBy } from 'lodash-es';

Mark packages as side-effect-free in package.json:

{ "sideEffects": ["*.css", "*.scss", "./src/renderer/global-setup.ts"] }

Lazy Loading

Route-Based Code Splitting

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

Dynamic Import in Main Process

export async function runHeavyAnalysis(data: Buffer): Promise<Result> {
  const sharp = await import('sharp'); // 25+ MB, load only when needed
  return sharp.default(data).resize(800, 600).toBuffer();
}

Bundle Analysis

// electron.vite.config.ts - Add visualizer in analyze mode
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  renderer: {
    plugins: [
      react(),
      process.env.ANALYZE && visualizer({
        filename: './bundle-report.html',
        open: true,
        gzipSize: true,
        template: 'treemap',
      }),
    ].filter(Boolean),
  },
});
ANALYZE=true npx electron-vite build

Size Monitoring in CI

#!/bin/bash
MAX_SIZE_MB=60
npx electron-vite build
SIZE=$(du -sm out/ | cut -f1)
echo "Bundle size: ${SIZE}MB (limit: ${MAX_SIZE_MB}MB)"
[ "$SIZE" -gt "$MAX_SIZE_MB" ] && echo "ERROR: Bundle exceeds limit!" && exit 1

ASAR Archives

ASAR packs app files into a single archive, improving Windows load time and hiding source from casual inspection.

// forge.config.js
module.exports = {
  packagerConfig: {
    asar: {
      unpack: '*.{node,dll,dylib,so}',
      unpackDir: '{node_modules/sharp,node_modules/better-sqlite3}',
    },
  },
};

Unpack native .node addons, files accessed via fs with absolute paths, large binaries that benefit from memory mapping, and executables spawned with child_process.

function getUnpackedPath(relativePath: string): string {
  if (app.isPackaged) {
    return path.join(process.resourcesPath, 'app.asar.unpacked', relativePath);
  }
  return path.join(__dirname, relativePath);
}

Native Module Handling

Rebuild native modules against Electron's Node.js headers:

npx @electron/rebuild

Keep native modules external to the bundler:

// electron.vite.config.ts
export default defineConfig({
  main: {
    build: {
      rollupOptions: {
        external: ['better-sqlite3', 'sharp', 'keytar', 'node-pty'],
      },
    },
  },
});

Asset Optimization

# Compress PNGs and convert to WebP
npx sharp-cli --input "assets/**/*.png" --output "assets-opt/" --format webp

# Subset fonts to Latin characters only (often 60-70% smaller)
npx glyphhanger --whitelist="US_ASCII" --subset="fonts/Inter.woff2"

# Generate platform-specific icons
npx electron-icon-builder --input=icon-source.png --output=./build

Excluding devDependencies

{
  "dependencies": {
    "electron-updater": "^6.0.0",
    "better-sqlite3": "^11.0.0"
  },
  "devDependencies": {
    "electron": "^33.0.0",
    "electron-vite": "^2.0.0",
    "typescript": "^5.0.0",
    "vitest": "^2.0.0"
  }
}

Only dependencies are included in the packaged app. Verify with:

npx electron-forge package
ls -la out/your-app-*/resources/app/node_modules/

See Also

Source: SKILL.md on GitHub

2 warnings16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive guidance, templates, reference scripts, and architectural configurations for developing secure Electron applications with React. All components adhere strictly to security standards, and all external tools, scripts, and configurations are handled using safe practices without any malicious or suspicious patterns.

  • Socket16d

    1 alert: gptSecurity

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    35/35 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 075cb8f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago
Other metadata
compatibility
Requires Deno for analysis scripts. Applicable to any Electron project using TypeScript and React.
metadata
{
  "author": "agent-skills",
  "version": "1.0",
  "domain": "development",
  "type": "utility",
  "mode": "assistive"
}

README badge

README badge for jwynia/agent-skills/electron-best-practices