Skip to content

feat(desktop): optimize cold development startup - #42722

Merged
Hona merged 11 commits into
anomalyco:v2from
Hona:desktop-devex
Aug 20, 2026
Merged

feat(desktop): optimize cold development startup#42722
Hona merged 11 commits into
anomalyco:v2from
Hona:desktop-devex

Conversation

@Hona

@Hona Hona commented Aug 15, 2026

Copy link
Copy Markdown
Member

Cutting cold desktop startup from 17.4 seconds to 6.7 seconds

bun dev:desktop originally took a median of 17,422 ms from command submission to a visible, actionable Home page. After rebasing the experiment onto current V2 and adapting it to the modular desktop architecture, the current five-run fresh-profile median is 6,670 ms.

That removes 10,753 ms, a 61.7% reduction, without reusing desktop build output, the desktop Vite cache, the desktop profile, the database, service registration, or the service process.

The window also no longer flashes from light to dark. Electron keeps it hidden until the renderer confirms that the persisted or default theme is fully applied.

Measurement contract

bun run bench:devex runs five serial samples of the exact bun dev:desktop command.

Before every sample, the harness removes desktop build output and packages/desktop/node_modules/.vite. It creates a fresh desktop profile, database, service state, registration, and process. The standard managed CLI service selects a new ephemeral loopback port. Dependencies, Bun's package cache, and Electron remain installed, matching a fresh worktree after bun install.

A sample completes only after all of these conditions are true:

  • Electron has revealed the native window.
  • The document is visible.
  • Home's Projects and Recent sessions regions are visible.
  • Search sessions is editable.
  • The empty recent-session state is visible.
  • The add-project control passes Playwright's actionability check.
  • The renderer emitted no page errors.
  • data-theme and data-color-scheme did not change after their initial resolved values.

The benchmark records raw samples, median, minimum, maximum, median absolute deviation, startup milestones, derived phases, service identity, stdout, stderr, and renderer errors.

Why the old path was slow

The old desktop path serialized build, Electron, local service, WSL setup, window creation, and renderer initialization. Home also pulled much of the session and draft graph into the initial renderer graph.

The original five-run median looked like this:

Milestone Original median
Desktop prepared 340 ms
Main bundle ready 1,562 ms
Electron started 2,658 ms
Service ready 5,808 ms
Renderer connected 6,212 ms
Window visible 16,254 ms
Home actionable 17,422 ms

A diagnostic crawl recorded 729 transforms and 1,725 module resolutions. A static session-route import accounted for another 278 transforms, including terminal, prompt, review, file-tree, timeline, model-dialog, and drag-and-drop code that Home did not need.

Changes

1. Use Vite 8 bundled development

The renderer now uses Vite's in-memory Rolldown development bundle instead of serving the initial graph as hundreds of browser-requested modules.

Package Version Registry status
Vite 8.2.1 latest
electron-vite 6.0.0-beta.1 newest Vite 8-compatible beta
vite-plugin-solid 2.11.14 latest
Tailwind 4.3.3 latest
@tailwindcss/vite 4.3.3 latest

electron-vite 5 supports Vite only through version 7. Version 6 beta is still the newest release with Vite 8 support. The config uses rolldownOptions, keeps the main process as ESM, emits preload as CJS, and preserves native node-pty externalization.

2. Keep session, draft, and File code outside Home

The current /server/:serverKey/session/:id and /new-session routes are lazy boundaries. The draft route retains current V2 provider ownership with ServerProvider, ModelsProvider, LocationProvider, and SessionUIProvider.

The session UI File component is lazy and preloads in parallel with either route. Restored session and draft routes preload while the desktop splash is still active, which avoids replacing faster Home startup with a blank first navigation.

3. Start renderer work and the local service concurrently

Electron starts the local managed service immediately after the application lock and final environment resolution, before app.whenReady, updater setup, IPC setup, and window preparation. This applies to development and packaged builds.

The renderer can compile and apply its theme while the service starts. Its existing initialization IPC boundary still prevents Home from using the local server before credentials are ready.

4. Keep WSL off the local critical path

WSL remains a remote-server concern. Local service readiness is published before WSL setup.

The current typed IPC contract includes a WSL initialization boundary. Early renderer WSL calls wait for that boundary, and subscribe/unsubscribe ordering remains safe if a component disposes while initialization is pending. WSL startup failure does not prevent local Home from opening.

5. Keep the standard CLI boundary

Desktop development continues to start the service through the normal CLI entrypoint:

packages/cli/src/index.ts serve --service --port 0

This PR does not add an alternate CLI entrypoint and has no final packages/cli or packages/core diff. CLI startup performance remains owned by the CLI team rather than being bypassed by desktop-specific argument parsing and process composition.

Electron main is the single service owner. The dev wrapper does not start another contender.

6. Defer code that cannot affect first Home paint

The cold path now defers:

  • Sentry when no DSN is configured
  • electron-updater when updates are disabled
  • migration status UI until local initialization exists
  • ZIP code until debug-log export
  • server and project edit dialogs until interaction
  • route-specific session and draft providers
  • the development DebugBar until the developer opens it

7. Narrow Tailwind discovery

Tailwind scans explicit app, desktop renderer, enterprise, session UI, storybook, and UI source roots. It excludes locale dictionaries, assets, and test files. The paths are resolved to their real src directories rather than package-root paths that did not exclude anything.

Tailwind 4.3.3 still needs a small guard around its bundled-development HMR hook because the upstream fix has not reached a stable release.

8. Remove the duplicate desktop renderer dictionaries

Native desktop strings already live in the app dictionaries and flow through createDesktopNativeBundle, preload IPC, and nativeT in the main process.

The obsolete renderer-only dictionaries and translation target are removed. The developer-only missing-root invariant uses a direct English diagnostic and no longer pulls a second locale graph into desktop startup.

9. Apply persisted theme state before reveal

The theme preload plugin now matches both web /oc-theme-preload.js and Electron ./oc-theme-preload.js paths. It runs in Vite's pre HTML phase, so the script is inline and executes before the renderer module.

The preload reads the same persisted keys as ThemeProvider:

  • opencode-theme-id, default oc-2
  • opencode-color-scheme, default system
  • opencode-theme-css-light
  • opencode-theme-css-dark

Explicit light and dark settings override the OS. System mode resolves prefers-color-scheme before paint. Custom themes restore their cached mode-specific CSS.

Electron now reveals a BrowserWindow only after both ready-to-show and the renderer's theme-applied signal. The main process also persists the renderer's computed background color for the next native window construction. This prevents the native surface, startup splash, and final app from showing different modes.

10. Reduce service discovery latency

Service discovery polls every 100 ms instead of 1,000 ms. The attempt count increases from 120 to 1,200, preserving the original approximate 120-second allowance rather than shortening the timeout.

Profiling evidence

The follow-up work used CDP tracing, Bun CPU profiling, service-registration probing, and isolated five-run A/B groups.

The initial renderer trace from attachment through Home reported:

Renderer category CPU time
Scripting 329 ms
Rendering 64 ms
Painting 4 ms
Idle 3,045 ms

The renderer was mostly waiting. Bun's service profile showed the normal source entry loading and initializing Effect, HTTP API schemas/routes, OpenTelemetry, Redis clients, Babel/Solid transform code, TypeScript, and OpenTUI modules. That work remains CLI/server-owned.

The registration probe found only 134 ms between the registration file appearing and the first authenticated health response. The first response was already 200 ready, so there is no multi-second provisional endpoint phase for desktop to exploit safely.

Isolated experiments:

Experiment Result Decision
Disable default DebugBar profiling service-ready to Home improved about 245 ms in repeated controls Keep DebugBar lazy and closed until requested
Move migration delay before its lazy import about 93 ms Reject as too small/noisy
Start service earlier inside Electron only about 6 ms on development Home Keep for packaged startup semantics, not a claimed dev gain
Run Electron validation and icon copy concurrently desktop preparation 395 ms to 218 ms Keep
Immediate dev service prewarm Home median 5.75 s on a fresh profile Reject after persisted-state validation
500 ms dev service prewarm Home median 5.03 s on a fresh profile Reject after persisted-state validation

The fresh profile made prewarm look safe because a contender won quickly. A persisted-profile run exposed the failure: wrapper prewarm plus Electron main produced four identical CLI contenders, saturated CPU, repeatedly replaced the registration, and took about 33 seconds. Removing wrapper prewarm restored one CLI owner and reached service readiness in 6.57 seconds with the real profile.

OPENCODE_PERFORMANCE_TRACE_DIR now enables the same CDP trace directly from bench:devex for future investigations.

Current results

Latest cold samples on current V2:

Run Command to actionable Home
1 5,710.14 ms
2 6,669.86 ms
3 6,693.20 ms
4 6,679.89 ms
5 5,918.58 ms
Metric Original Current
Minimum 17,207.89 ms 5,710.14 ms
Median 17,422.46 ms 6,669.86 ms
Maximum 17,700.95 ms 6,693.20 ms
Median absolute deviation 35.73 ms 23.34 ms
Median time removed 10,752.60 ms
Median reduction 61.7%

Current median milestones:

Milestone Median
Desktop prepared 191.28 ms
Main bundle ready 1,059.72 ms
Preload bundle ready 1,079.13 ms
Renderer server ready 1,138.05 ms
Electron started 2,201.84 ms
Renderer connected 2,800.02 ms
Window visible with theme applied 4,030.07 ms
Service ready 5,524.99 ms
Renderer initialization ready 5,525.83 ms
Home actionable 6,669.86 ms

The median visible-window-to-actionable-Home interval is 2,642.63 ms. During that interval the correctly themed loading splash remains visible while the single standard CLI service starts. Every final sample kept one stable resolved theme and color scheme through Home readiness.

Verification

  • rebased onto upstream/v2 at e2d9376614
  • full pre-push repository typecheck: 33/33 tasks passed
  • app, desktop, CLI, client, and complete app E2E typechecks passed
  • app and desktop production builds passed
  • complete app E2E suite: 97/97 passed
  • Node build and compiled service smoke passed with the repository Vite 7 toolchain
  • Vite 8, Tailwind 4.3, and vite-plugin-solid 2.11.14 are pinned only in app/desktop
  • final cold desktop benchmark: 5/5 samples passed
  • benchmark renderer errors: 0
  • benchmark theme or mode transitions: 0
  • theme preload coverage: defaults, explicit light, explicit dark, system dark, custom light, and custom dark passed
  • prompt placeholder browser assertion verifies computed U+200B content
  • WSL server and startup behavior: 9 tests passed
  • updater and window registry: 19 tests passed
  • translation workflow: 15 tests passed
  • renderer HTML and preload-path checks: 4 tests passed
  • benchmark-owned services and temporary profiles removed after every run
  • final diff against upstream/v2: no packages/cli or packages/core changes
  • git diff --check passed

Risks

  • Vite bundled development remains experimental, so Vite is pinned exactly.
  • electron-vite 6.0.0-beta.1 remains a prerelease because no stable electron-vite version supports Vite 8 yet.
  • The Tailwind bundled-development HMR guard depends on a private plugin hook and should be removed when the upstream fix is released.
  • Electron remains on upstream's 42.3.3. Newer Electron releases were intentionally excluded so this PR does not change the runtime and benchmark variable at the same time.
  • The standard source CLI remains the largest cold phase. A persisted-profile validation reached service readiness in 6.57 seconds with one owner; reducing the service's internal startup cost belongs to CLI/server work.
  • The benchmark ran with installed Bun 1.4.0; dependency installation and lockfile generation used repository-pinned Bun 1.3.14.

@Hona
Hona force-pushed the desktop-devex branch 3 times, most recently from 65dbb31 to 8afbec1 Compare August 20, 2026 06:22
@Hona
Hona marked this pull request as ready for review August 20, 2026 07:33
@Hona
Hona requested a review from Brendonovich as a code owner August 20, 2026 07:33
Copilot AI lite review requested due to automatic review settings August 20, 2026 07:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Hona
Hona merged commit 2a7d072 into anomalyco:v2 Aug 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants