Skip to content

Deep Dive: Go + Wails + chromedp #6

@chhot2u

Description

@chhot2u

Go + Wails + chromedp — Deep Dive Analysis

Overview

Go backend with native goroutine concurrency, Wails for desktop UI (uses system WebView), and chromedp for Chrome DevTools Protocol-based browser control. Second-ranked solution (7.85/10).


Architecture

┌──────────────────────────────────────┐
│          Wails Desktop App           │
│  ┌────────────────────────────────┐  │
│  │   Go Backend                   │  │
│  │   - Goroutine pool (100)       │  │
│  │   - Channel-based task queue   │  │
│  │   - chromedp browser control   │  │
│  │   - Proxy pool manager         │  │
│  │   - SQLite (go-sqlite3)        │  │
│  ├────────────────────────────────┤  │
│  │   System WebView (UI)          │  │
│  │   - Svelte/React dashboard     │  │
│  │   - Wails bindings (RPC)       │  │
│  │   - Real-time updates          │  │
│  └────────────────────────────────┘  │
└──────────────────────────────────────┘
        │
        ▼
┌──────────────────────────────────────┐
│   Chrome/Chromium (headless)         │
│   - 100 browser contexts via CDP    │
│   - Each context: own proxy         │
│   - Controlled via DevTools Protocol│
└──────────────────────────────────────┘

Key Dependencies

// go.mod
require (
    github.com/chromedp/chromedp v0.10.x
    github.com/wailsapp/wails/v2 v2.9.x
    github.com/mattn/go-sqlite3 v1.14.x
    golang.org/x/sync v0.9.x  // errgroup, semaphore
)

Proxy per Task Implementation

func (a *App) RunTask(config TaskConfig) (TaskResult, error) {
    // Create allocator with proxy
    allocCtx, cancel := chromedp.NewExecAllocator(context.Background(),
        chromedp.Flag("headless", true),
        chromedp.ProxyServer(config.Proxy.Server),
    )
    defer cancel()

    ctx, cancel := chromedp.NewContext(allocCtx)
    defer cancel()

    // Set proxy auth if needed
    if config.Proxy.Username != "" {
        chromedp.ListenTarget(ctx, func(ev interface{}) {
            if e, ok := ev.(*fetch.EventAuthRequired); ok {
                go func() {
                    fetch.ContinueWithAuth(e.RequestID, &fetch.AuthChallengeResponse{
                        Response: fetch.AuthChallengeResponseResponseProvideCredentials,
                        Username: config.Proxy.Username,
                        Password: config.Proxy.Password,
                    }).Do(cdp.WithExecutor(ctx, chromedp.FromContext(ctx).Target))
                }()
            }
        })
    }

    var result TaskResult
    err := chromedp.Run(ctx, executeSteps(config.Steps, &result)...)
    return result, err
}

Concurrency Model

func (a *App) RunBatch(tasks []TaskConfig) []TaskResult {
    results := make([]TaskResult, len(tasks))
    sem := semaphore.NewWeighted(100) // max 100 concurrent
    var wg sync.WaitGroup

    for i, task := range tasks {
        wg.Add(1)
        go func(idx int, t TaskConfig) {
            defer wg.Done()
            sem.Acquire(context.Background(), 1)
            defer sem.Release(1)

            result, err := a.RunTask(t)
            if err != nil {
                result = TaskResult{Error: err.Error()}
            }
            results[idx] = result

            // Notify UI of progress
            runtime.EventsEmit(a.ctx, "task:complete", idx, result)
        }(i, task)
    }

    wg.Wait()
    return results
}

Strengths

  • Native concurrency: Goroutines handle 100+ tasks with ~1.5GB RAM
  • Fast: Compiled binary, no runtime overhead
  • Small bundle: ~15MB (no bundled browser for UI)
  • Channel-based queue: Built-in Go channels, no external queue needed
  • Type safety: Strong typing catches errors at compile time
  • Single binary: Easy distribution
  • Low memory per goroutine: ~8KB stack vs ~1MB per OS thread

Weaknesses

  • chromedp limitations: No auto-wait (must implement manually), no smart selectors
  • Chromium only: No Firefox/WebKit support
  • No network interception: CDP fetch domain is lower-level than Playwright
  • Manual auth handling: Must implement auth flows manually
  • Steeper learning curve: Go + Wails + CDP knowledge needed
  • WebView inconsistency: Different rendering per OS
  • Less automation tooling: No codegen, no trace viewer

Resource Estimates (100 tasks)

Resource Estimate
RAM ~1.5 GB
CPU 2-4 cores sufficient
Disk ~15MB app + system Chrome
Startup <1s
Goroutine overhead ~8KB each

When to Choose This Stack

Performance is top priority
✅ Team knows Go
✅ Need minimal resource usage
Chromium-only testing is acceptable
✅ Don't need advanced automation (auto-wait, codegen)
✅ Want single binary distribution

❌ Avoid if: need multi-browser, need rich automation API, team doesn't know Go, or need network mocking


Verdict: 7.85/10

Best raw performance. Ideal if your team knows Go and you only need Chromium.

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