Skip to content

perf(desktop): show the first window the moment Electron is ready - #49869

Merged
Hona merged 3 commits into
v2from
early-window
Sep 19, 2026
Merged

Hona merged 3 commits into
v2from
early-window

Conversation

@Hona

@Hona Hona commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

Users saw nothing for ~900 ms after launch. Three things were serial:

  1. index.ts did await import("./desktop") — Electron holds ready until the entry module finishes, so the whole main bundle evaluated before Chromium was even ready
  2. windows were created only after every layer had built
  3. they were show: false until the renderer had painted and reported its theme colour over IPC (and the renderer waits for the first-launch check before doing that)
flowchart LR
  subgraph before
    a[Node ready 60] --> b[main bundle evaluates] --> c[ready ~390] --> d[layers] --> e[window created ~520, hidden] --> f[renderer paints + theme IPC] --> g[shown ~890]
  end
  subgraph after
    h[Node ready 60] --> i[tiny entry: configure, lock, scheme] --> j[ready ~220] --> k[window shown ~250] --> l[import desktop] --> m[layers adopt window, loadURL] --> n[renderer paints into the visible window ~810]
  end
Loading

What moves where

  • index.ts is now small: configureApplication() (paths, app id, command-line switches, test root — everything that must precede ready, now synchronous in lifecycle/configure.ts), the single-instance lock, registerRendererScheme() (windows/scheme.ts; the privilege call must precede ready), then on ready: createEarlyWindow() and only then import("./desktop")
  • windows/early.ts creates the first restored window: id from the persisted window list, bounds from its state file, background from the persisted theme colour, the same frame/preload options as windowAppearance (shared via windows/defaults.ts), and shows it. Closing it before adoption quits the app.
  • restoreWindows() adopts it (takeEarlyWindow()): security wiring, registry, loadURL as before; reveal() is already satisfied. New windows opened later keep today's behaviour.
  • Import ordering was measured both ways: starting the import before ready is worse on every milestone (window 276 → 434 ms) because the module graph evaluates on the thread Chromium needs to finish initialising.
  • electron-context-menu moves out of configuration and installs after restoreWindows() (it attaches to existing and future windows), which also keeps its dependency tree off the startup path — supersedes perf(desktop): install the context menu after the first window exists #49794
  • The bench reads the early window's shownAt from the adoption log line, since the logger doesn't exist yet when the window is shown.

Measured with bun run bench:startup --compare (interleaved launches, warm service, median of 5, current v2 as baseline):

ms since spawn v2 this PR
window visible 893 251
first paint 1,065 812
first log line 393 459
renderer process 529 588
shell visible 1,012 1,124
renderer idle 1,626 1,595

The trade: the frame is on screen 640 ms earlier and the splash paints 250 ms earlier; the main bundle now starts after the window exists, so the shell lands ~100 ms later. Main-thread CPU in the first half second is saturated either way — this changes what the user sees first, not how much work there is. Reducing the bundle and layers (the other PRs in this series) shortens the remaining gap.

Also tried, parked on branch early-renderer: loading the renderer into the early window before the layers. Renderer process 600 → 259 ms, but the main thread then serves its chunks and the RenderProcessHost while the bundle evaluates: first paint 814 → 778, shell 1,135 → 1,087, idle 1,676 → 1,578, while the first log line slips 457 → 605 and service ready 578 → 856 (bad for cold starts). Not a clear win; separate decision.

The ~250 ms is close to the platform floor: Node bootstrap completes at ~60 ms, Electron's own JS init and Chromium's main-loop setup take it to ready at ~220, BrowserWindow is ~30.

@Hona
Hona requested a review from Brendonovich as a code owner September 19, 2026 00:08
Copilot AI lite review requested due to automatic review settings September 19, 2026 00:08

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.

The entry module awaited the whole main bundle, which held back
Electron's ready event; windows were then created only after every
layer had built, hidden until the renderer had painted and reported its
theme. Users saw nothing for ~900 ms.

The entry is now a small module that configures the app, takes the
single-instance lock and registers the renderer scheme (all of which must
precede ready), then on ready creates and shows the first restored window
- bounds from its state file, background from the persisted theme colour
- and only then imports the rest of the main process, which adopts that
window in restoreWindows() and loads the renderer into it. Starting the
import before ready measured worse: the module graph evaluates on the
thread Chromium needs to finish initialising.

Configuration moves from an Effect in the lifecycle layer to a
synchronous configure.ts; the renderer scheme constants move to
scheme.ts; frame defaults shared by the early window and windowAppearance
live in defaults.ts. The bench reads the early window's shownAt from the
adoption log line.

Packaged build, warm service, interleaved with the v2 baseline, median
of 5: window visible 893 -> 251 ms, first paint 1065 -> 812 ms; the
first log line and the shell land ~65-100 ms later because the bundle now
starts after the window exists.
Kill the launched app and stop the bench service when a run throws, so a
launch that never produced a renderer cannot leave an instance behind
that later launches hand off to. Compared builds must bundle the same
CLI version (read from opencode-cli.version), otherwise the desktop
restarts the service on the mismatch and the comparison is meaningless.
The context menu call lived in configureApplication and was lost when
that moved to the entry module. Install it after restoreWindows(), where
electron-context-menu attaches to existing and future windows alike,
which also keeps its dependency tree off the startup path (#49794).
@Hona
Hona merged commit e1e2efe into v2 Sep 19, 2026
8 checks passed
@Hona
Hona deleted the early-window branch September 19, 2026 00:41
jinhuang712 pushed a commit to jinhuang712/opencode that referenced this pull request Sep 26, 2026
divyanshusingh2903 added a commit to divyanshusingh2903/forge that referenced this pull request Oct 21, 2026
Create the main windows before waiting on the sidecar health check; the
renderer already awaits the sidecar credentials over IPC, so it can paint
while the server boots. Also run the login-shell environment probe
asynchronously so it overlaps Electron startup and window creation instead
of blocking the main process.

On a dev build the renderer now reaches the server request at ~0.8s
instead of ~5.2-6.2s after launch.

Window ordering adapted from upstream 5b565e8 (anomalyco#49763) and
e1e2efe (anomalyco#49869); the async shell probe is Forge's own.
divyanshusingh2903 added a commit to divyanshusingh2903/forge that referenced this pull request Oct 21, 2026
Create the main windows before waiting on the sidecar health check; the
renderer already awaits the sidecar credentials over IPC, so it can paint
while the server boots. Also run the login-shell environment probe
asynchronously so it overlaps Electron startup and window creation instead
of blocking the main process.

On a dev build the renderer now reaches the server request at ~0.8s
instead of ~5.2-6.2s after launch.

Window ordering adapted from upstream 5b565e8 (anomalyco#49763) and
e1e2efe (anomalyco#49869); the async shell probe is Forge's own.

Upstream: anomalyco/opencode@5b565e8923 (anomalyco#49763)
Upstream: anomalyco/opencode@e1e2efe8ec (anomalyco#49869)
divyanshusingh2903 added a commit to divyanshusingh2903/forge that referenced this pull request Oct 21, 2026
Create the main windows before waiting on the sidecar health check; the
renderer already awaits the sidecar credentials over IPC, so it can paint
while the server boots. Also run the login-shell environment probe
asynchronously so it overlaps Electron startup and window creation instead
of blocking the main process.

On a dev build the renderer now reaches the server request at ~0.8s
instead of ~5.2-6.2s after launch.

Window ordering adapted from upstream 5b565e8 (anomalyco#49763) and
e1e2efe (anomalyco#49869); the async shell probe is Forge's own.

Upstream: anomalyco/opencode@5b565e8923 (anomalyco#49763)
Upstream: anomalyco/opencode@e1e2efe8ec (anomalyco#49869)
divyanshusingh2903 added a commit to divyanshusingh2903/forge that referenced this pull request Oct 21, 2026
Create the main windows before waiting on the sidecar health check; the
renderer already awaits the sidecar credentials over IPC, so it can paint
while the server boots. Also run the login-shell environment probe
asynchronously so it overlaps Electron startup and window creation instead
of blocking the main process.

On a dev build the renderer now reaches the server request at ~0.8s
instead of ~5.2-6.2s after launch.

Window ordering adapted from upstream 5b565e8 (anomalyco#49763) and
e1e2efe (anomalyco#49869); the async shell probe is Forge's own.

Upstream: anomalyco/opencode@5b565e8923 (anomalyco#49763)
Upstream: anomalyco/opencode@e1e2efe8ec (anomalyco#49869)
divyanshusingh2903 added a commit to divyanshusingh2903/forge that referenced this pull request Oct 21, 2026
Create the main windows before waiting on the sidecar health check; the
renderer already awaits the sidecar credentials over IPC, so it can paint
while the server boots. Also run the login-shell environment probe
asynchronously so it overlaps Electron startup and window creation instead
of blocking the main process.

On a dev build the renderer now reaches the server request at ~0.8s
instead of ~5.2-6.2s after launch.

Window ordering adapted from upstream 5b565e8 (anomalyco#49763) and
e1e2efe (anomalyco#49869); the async shell probe is Forge's own.

Upstream: anomalyco/opencode@5b565e8923 (anomalyco#49763)
Upstream: anomalyco/opencode@e1e2efe8ec (anomalyco#49869)
Co-authored-by: Luke Parker <[email protected]>
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