Skip to content

Deep Dive: Electron + Playwright #2

@chhot2u

Description

@chhot2u

Electron + Playwright — Deep Dive Analysis

Overview

Electron provides a Chromium-based desktop shell. Playwright runs as a Node.js library for browser automation. Combined, you get a desktop app with powerful browser control.


Architecture

┌──────────────────────────────┐
│     Electron Main Process    │
│  ┌────────────────────────┐  │
│  │   Task Scheduler       │  │
│  │   (BullMQ / custom)    │  │
│  ├────────────────────────┤  │
│  │   Playwright Engine    │  │
│  │   - 1 Browser instance │  │
│  │   - 100 BrowserContexts│  │
│  │   - Per-context proxy  │  │
│  ├────────────────────────┤  │
│  │   IPC Bridge           │  │
│  └────────────────────────┘  │
├──────────────────────────────┤
│    Electron Renderer         │
│  ┌────────────────────────┐  │
│  │   React/Vue Dashboard  │  │
│  │   - Task list & status │  │
│  │   - Live preview pane  │  │
│  │   - Config panel       │  │
│  │   - Logs viewer        │  │
│  └────────────────────────┘  │
└──────────────────────────────┘

Key Dependencies

{
  "electron": "^33.x",
  "playwright-core": "^1.49.x",
  "bullmq": "^5.x",
  "better-sqlite3": "^11.x",
  "react": "^19.x",
  "electron-builder": "^25.x"
}

Proxy per Task Implementation

const browser = await chromium.launch();

// Each task gets its own isolated context with unique proxy
async function createTask(proxyConfig: ProxyConfig) {
  const context = await browser.newContext({
    proxy: {
      server: proxyConfig.server,       // e.g., "http://proxy-us-east:8080"
      username: proxyConfig.username,
      password: proxyConfig.password,
    },
    locale: proxyConfig.locale,          // e.g., "en-US"
    timezoneId: proxyConfig.timezone,    // e.g., "America/New_York"
  });
  
  const page = await context.newPage();
  return { context, page };
}

Concurrency Model

import { Worker } from 'worker_threads';

// Main process spawns worker threads for CPU-bound tasks
// Playwright contexts share single browser (memory efficient)
const CONCURRENCY_LIMIT = 100;
const semaphore = new Semaphore(CONCURRENCY_LIMIT);

async function runTask(taskConfig: TaskConfig) {
  await semaphore.acquire();
  try {
    const { context, page } = await createTask(taskConfig.proxy);
    await executeSteps(page, taskConfig.steps);
    await context.close();
  } finally {
    semaphore.release();
  }
}

Strengths

  • Rich Playwright API: Auto-wait, smart selectors, network interception
  • Multi-browser: Test on Chromium, Firefox, WebKit
  • Mature ecosystem: electron-builder, auto-update, crash reporting
  • Context isolation: Each task fully isolated (cookies, storage, proxy)
  • Screenshot/video: Built-in recording per context
  • Familiar stack: JavaScript/TypeScript throughout

Weaknesses

  • Heavy bundle: ~200MB+ (ships Chromium twice — Electron + Playwright)
  • RAM hungry: ~3-4GB for 100 contexts + Electron overhead
  • Single-threaded gotcha: Main process can bottleneck if not using workers
  • Slow startup: ~3s to launch
  • Security surface: Chromium vulnerabilities affect app

Resource Estimates (100 tasks)

Resource Estimate
RAM 3-4 GB
CPU 4-8 cores recommended
Disk ~500MB installed
Network Depends on tasks
Startup ~3s app + ~2s browser

When to Choose This Stack

✅ You want a desktop app with native feel
✅ You need multi-browser testing
✅ Your team knows JavaScript/TypeScript
✅ You need rich browser automation (auto-wait, selectors, interception)
✅ Task count stays under ~150 concurrent

❌ Avoid if: you need >200 concurrent tasks, bundle size matters, or server deployment


Verdict: 6.65/10

Good feature set but heavy resource usage and large bundle. Better options exist for pure scale.

References issue #1 for full comparison

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions