Gelectron
A drop-in replacement for Electron using native web views (WKWebView / WebView2 / WebKitGTK) instead of Chromium
See the benchmark results and comparisons for the latest performance data.
Why Gelectron?
Electron bundles Chromium — ~300 MB per app with 500+ MB RSS. Gelectron uses the OS-native web view (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux) via the wry and tao Rust crates, producing smaller binaries with dramatically lower memory usage.
| Feature | Electron | Gelectron |
|---|---|---|
| Rendering Engine | Chromium | Native WebView (WKWebView / WebView2 / WebKitGTK) |
| Total RSS (process tree) | ~588 MB | ~131 MB |
| Binary Size | ~300 MB | ~3 MB |
| Language | C++ / Node.js | Rust / Node.js |
| API Compatibility | Native | Drop-in replacement |
| Node.js Integration | Built-in | Spawned child process or WebView-only |
| Auto Updater | Built-in | Stub (no-update-safe fallback) |
Quick Start
Prerequisites
- Rust 1.75+ (
rustup.rs) - Node.js 18+
- npm
Build & Run
git clone https://github.com/mileswolfallen2/gelectron.git
cd gelectron
npm install
# Build the standalone native binary
cargo build --release -p gelectron
# Run the demo app
cargo run --release -p gelectron -- demo/
# Or run any Electron app
cargo run --release -p gelectron -- /path/to/electron-app
CLI (Node.js fallback)
If you don't want to build the Rust binary, the CLI can fall back to a pure-Node.js shim:
node cli/gelectron.js /path/to/electron-app
In fallback mode no real window is created — only the JS API layer loads. Use the native binary for actual rendering.
How It Works
Gelectron has two execution paths:
1. Native Binary (gelectron-app crate)
A standalone Rust binary using tao (windowing) and wry (WebView). It has two modes:
Node.js mode (default):
- Reads the target app's
package.jsonto find the main script - Generates a Node.js setup script that patches
require('electron')to point at Gelectron's JS compatibility layer - Spawns Node.js as a child process with piped stdin/stdout
- Runs a tao event loop with wry WebView windows
- Communicates with Node.js via JSON-line IPC (
create-window,load-url,ipc-message, …)
WebView-only mode (--no-node):
- Loads the JS compatibility layer directly inside the WebView
- The app's main script runs inside the WKWebView JavaScript context
- No Node.js process is spawned — saves ~50 MB RSS
- Some APIs (native dialogs, clipboard, screen info) communicate directly from the WebView to the Rust binary via
window.ipc.postMessage()
2. Node.js Fallback (cli/gelectron.js)
When the native binary is not built, the CLI falls back to pure Node.js:
- Patches
Module._resolveFilenamesorequire('electron')resolves to Gelectron's shim - Loads the app's main script — the app runs against the JS compatibility layer
- No real window is created (API-only mode)
Architecture
┌──────────────────────────────────────────────────┐
│ Gelectron App │
│ (HTML / CSS / JS + package.json) │
│ (Same code as Electron apps) │
└────────────────────┬─────────────────────────────┘
│
┌────────────────────▼─────────────────────────────┐
│ Gelectron Runtime │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ electron compat layer (JavaScript) │ │
│ │ app · BrowserWindow · Menu · Tray │ │
│ │ ipcMain · ipcRenderer · contextBridge │ │
│ │ dialog · shell · notification │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ gelectron-app (Rust standalone binary) │ │
│ │ tao · windowing │ │
│ │ wry · WebView (WKWebView / WebView2 / │
│ │ WebKitGTK) │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
Supported Electron APIs
Main Process
| Module | Status |
|---|---|
app |
Full lifecycle, paths, command line, dock (macOS), whenReady() |
BrowserWindow |
Create, show/hide, resize, loadURL, loadFile, events, webContents |
ipcMain |
handle(), on(), removeHandler(), event emission |
Menu |
buildFromTemplate(), popup(), setApplicationMenu() |
MenuItem |
All types (normal, checkbox, separator, submenu, role) |
Tray |
Create, tooltip, context menu, click events |
dialog |
showOpenDialog(), showSaveDialog(), showMessageBox(), showErrorBox() |
shell |
openExternal(), showItemInFolder(), openPath() |
Notification |
Full API + native OS notifications (macOS Notification Center, Windows Toasts, Linux D-Bus); click/action/reply/close/failed events |
nativeImage |
Create from path/buffer, resize, crop, PNG/JPEG export |
safeStorage |
Encrypt/decrypt via system keyring |
contextBridge |
exposeInMainWorld() for secure preload |
webContents |
send(), executeJavaScript(), openDevTools(), navigation |
Renderer Process
| Module | Status |
|---|---|
ipcRenderer |
invoke(), send(), on(), removeListener() |
Compatibility Shims
| Module | Status |
|---|---|
screen |
getPrimaryDisplay() (stub) |
clipboard |
Full API (readText/writeText/readHTML/readRTF/readImage/readBookmark/readFindText/clear/availableFormats/has) |
systemPreferences |
Basic stubs |
powerMonitor |
Event stubs |
globalShortcut |
Register/unregister stubs |
session |
Cookies, protocol, permissions (stub) |
net |
fetch() proxy |
autoUpdater |
No-op stub (reports "no update available") |
Demo App
A minimal demo that renders HTML/CSS/JS in a real window:
cargo run --release -p gelectron -- demo/
The demo includes:
- Interactive counter (DOM updates via JS)
- Live clock driven by
requestAnimationFrame - Animated canvas with moving shapes
- CSS grid, gradients, transitions, and flexbox
Demo source
demo/
├── package.json # { "main": "main.js" }
├── main.js # Creates BrowserWindow, loads index.html
└── index.html # HTML + CSS + JavaScript
Running the demo
demo/main.js:
const { app, BrowserWindow } = require('electron');
const path = require('path');
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 900, height: 680 });
win.loadFile(path.join(__dirname, 'index.html'));
});
app.on('window-all-closed', () => app.quit());
Project Structure
gelectron/
├── Cargo.toml # Rust workspace root
├── package.json # npm package
├── cli/
│ └── gelectron.js # CLI entry point (Node.js fallback)
├── src/
│ └── electron/ # JS Electron compatibility layer
│ ├── index.js # Main exports (require('electron'))
│ ├── app.js # app lifecycle
│ ├── browser-window.js # BrowserWindow + WebContents
│ ├── ipc-main.js # ipcMain
│ ├── ipc-renderer.js # ipcRenderer
│ ├── context-bridge.js # contextBridge
│ ├── menu.js # Menu + MenuItem
│ ├── tray.js # Tray
│ ├── dialog.js # File/message dialogs
│ ├── shell.js # Shell operations
│ ├── notification.js # Notifications
│ ├── native-image.js # Image handling
│ ├── safe-storage.js # Encryption
│ ├── web-contents.js # webContents utilities
│ ├── auto-updater.js # autoUpdater stub
│ ├── native-bridge.js # IPC to Rust binary
│ ├── preload-loader.js # Preload injection
│ └── runtime.js # Node.js fallback runtime
├── crates/
│ ├── gelectron-core/ # N-API addon (Rust → Node.js)
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs # N-API entry: init(), get_platform()
│ │ ├── app.rs # App lifecycle (native)
│ │ ├── browser_window.rs # Window management (native)
│ │ ├── servo_host.rs # Servo engine hooks (stub, not in use)
│ │ ├── event_loop.rs # Event loop bridge
│ │ ├── ipc.rs # IPC bridge
│ │ ├── protocol.rs # Custom protocol handler
│ │ ├── menu.rs # Native menus (muda)
│ │ ├── tray.rs # System tray (tray-icon)
│ │ ├── dialog.rs # File dialogs (rfd)
│ │ ├── shell.rs # Shell operations
│ │ ├── notification.rs # Notifications (notify-rust)
│ │ ├── native_image.rs # Image processing (image)
│ │ ├── safe_storage.rs # Secure storage (keyring)
│ │ ├── context_bridge.rs # Context bridge (native)
│ │ └── web_contents.rs # WebContents (native)
│ └── gelectron-app/ # Standalone native binary
│ ├── Cargo.toml
│ └── src/
│ └── main.rs # tao + wry event loop, Node.js spawner
├── demo/ # Demo app
│ ├── package.json
│ ├── main.js
│ └── index.html
├── packager/ # gelectron-packager CLI
│ ├── package.json
│ └── bin/
│ └── gelectron-packager.js # Packaging tool
└── npm/
└── darwin-arm64/ # Platform-specific npm packages
Packaging for Distribution
Use gelectron-packager to build standalone executables for Mac, Windows, and Linux:
# Install the packager
cd packager && npm link && cd ..
# Package for current platform
gelectron-packager --dir ./demo --name MyApp
# Package for a specific platform
gelectron-packager --dir ./my-app --name MyApp --platform darwin --arch arm64
gelectron-packager --dir ./my-app --name MyApp --platform win32 --arch x64
gelectron-packager --dir ./my-app --name MyApp --platform linux --arch x64
What the packager does
- Finds your built gelectron binary (
target/release/gelectron) - Downloads a bundled Node.js runtime (~20 MB) for the target platform
- Copies your app source and
node_modules - Includes the Electron compatibility layer (
src/electron/) - Creates a self-contained, standalone distributable — no additional files needed at runtime:
- macOS:
.appbundle (double-click to run, can be moved anywhere) - Windows: Directory with
.exe+.batlauncher - Linux: Directory with launcher script +
.desktopfile
- macOS:
The packaged app bundles the Rust binary, Node.js runtime, your source code,
node_modules, and the Electron compat layer. You can delete the original project files and the packaged app will still run.
macOS .app bundle structure
MyApp.app/
Contents/
MacOS/
MyApp # Bash launcher (sets PATH, calls gelectron-bin)
gelectron-bin # Rust binary (tao + wry)
node # Bundled Node.js
compat/ # Electron compatibility layer
node_modules/ # Production dependencies
Resources/
app/ # Your app source
Info.plist
Building for Production
Standalone binary (recommended)
cargo build --release -p gelectron
N-API addon (for Node.js integration)
cargo build --release -p gelectron-core
The N-API addon compiles to a .node file that can be loaded directly into Node.js.
Publishing to npm
Gelectron uses napi-rs to produce platform-specific native addons. The main gelectron npm package ships platform-specific optional packages so that npm install gelectron automatically pulls the right binary for the user's OS.
Prerequisites
- Rust 1.75+ (
rustup.rs) - Node.js 18+
- npm
- An npm account with publish access
- Each target platform needs to be built on that platform (or via CI)
Step 1: Build the native addon for your platform
# Build the N-API addon (produces crates/gelectron-core/*.node)
npm run build
# Or build with debug symbols for development
npm run build:debug
This compiles the Rust N-API addon (gelectron-core) into a .node file that Node.js can load.
Step 2: Create platform-specific npm packages
For each platform you want to support, create a directory under npm/ with a package.json:
# Example for macOS ARM64
mkdir -p npm/darwin-arm64
cat > npm/darwin-arm64/package.json << 'EOF'
{
"name": "gelectron-darwin-arm64",
"version": "0.1.0",
"description": "Gelectron native addon for macOS ARM64",
"main": "index.darwin-arm64.node",
"files": ["index.darwin-arm64.node"],
"os": ["darwin"],
"cpu": ["arm64"],
"license": "MIT"
}
EOF
# Copy the built .node file
cp crates/gelectron-core/gelectron_core.darwin-arm64.node npm/darwin-arm64/
Repeat for each platform:
| Directory | os | cpu |
|---|---|---|
npm/darwin-arm64/ |
darwin |
arm64 |
npm/darwin-x64/ |
darwin |
x64 |
npm/win32-x64-msvc/ |
win32 |
x64 |
npm/win32-arm64-msvc/ |
win32 |
arm64 |
npm/linux-x64-gnu/ |
linux |
x64 |
npm/linux-arm64-gnu/ |
linux |
arm64 |
Step 3: Publish platform packages first
Each platform package must be published before the main package:
# Publish each platform package
npm publish npm/darwin-arm64 --access public
npm publish npm/darwin-x64 --access public
npm publish npm/win32-x64-msvc --access public
# ... etc for each platform
Step 4: Prepare and publish the main package
# Run prepublish hook (generates napi artifacts metadata)
npm run prepublishOnly
# Publish the main package
npm publish --access public
Using napi-rs CLI (recommended)
The @napi-rs/cli handles cross-compilation and artifact management:
# Install napi-rs CLI globally (if not already installed)
npm install -g @napi-rs/cli
# Build for all configured targets
napi build --platform --release
# Generate artifact metadata for npm publishing
napi prepublish -t npm
# Create a GitHub release with platform binaries
napi artifacts
CI/CD Publishing (recommended)
For multi-platform publishing, use GitHub Actions to build on each OS:
# .github/workflows/publish.yml
name: Publish to npm
on:
push:
tags: ['v*']
jobs:
build:
strategy:
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: napi build --platform --release --target ${{ matrix.target }}
- run: napi prepublish -t npm
- uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.target }}
path: npm/
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Quick publish (single platform)
If you only need to publish for your current platform:
# Build
npm run build
# Preview what will be published
npm pack --dry-run
# Publish
npm run prepublishOnly
npm publish --access public
Tip: Use
npm packto create a tarball locally and inspect it before publishing. Runnpm packand thentar -tzf gelectron-0.1.0.tgzto verify the contents.
Testing with OmniEmu2.0
OmniEmu2.0 is a full Electron app used to validate Gelectron compatibility:
# From the gelectron directory
cargo run --release -p gelectron -- /path/to/OmniEmu2.0
Key APIs exercised by OmniEmu2.0:
app,BrowserWindow,Tray,Menu,nativeImage,dialogelectron-updater(autoUpdater stub)contextBridge,ipcRenderer- File loading (
loadFile), window events
CLI Options
gelectron <path-to-app> # Run an Electron app
gelectron <file.js> # Run a main process script directly
gelectron --version # Print version
gelectron --help # Show help
Environment Variables
| Variable | Description |
|---|---|
GELECTRON_DEV=1 |
Enable development mode |
GELECTRON_LOG=1 |
Enable verbose logging |
VITE_DEV_SERVER_URL=<url> |
Connect to a Vite dev server |
RUST_LOG=info |
Enable Rust-side logging |
Known Limitations
- Auto-updater is a no-op stub (returns "no update available")
- Some Electron APIs are stubs (marked in compatibility table)
- Preload scripts are injected via WebView init scripts, not true Electron preload isolation
- Native menu rendering is macOS-only (Windows/Linux fall back to JS-only menus)
Roadmap
- JS Electron API compatibility layer
- Standalone native binary (tao + wry)
- Node.js fallback runtime
- JSON-line IPC between Rust and Node.js
- WebView-only mode (
--no-node) electron-updatercompatibility- Multi-window support
- Custom protocol handlers (
gelectron://) - DevTools integration
- App sandboxing
- Package/distribution tooling
- Performance benchmarks vs Electron
- Cross-platform verification (Windows, Linux)
Contributing
- Fork the repo
- Create a feature branch
- Make your changes
- Run
cargo build --release -p gelectronand test withcargo run --release -p gelectron -- demo/ - Submit a PR
License
MIT — see LICENSE