Skip to content

perf(desktop): boot the renderer alongside the main process - #50044

Closed
Hona wants to merge 4 commits into
anomalyco:v2from
Hona:desktop-startup
Closed

Hona wants to merge 4 commits into
anomalyco:v2from
Hona:desktop-startup

Conversation

@Hona

@Hona Hona commented Sep 19, 2026

Copy link
Copy Markdown
Member

The first window's renderer used to start only after every main-process layer had been built, then wait for the IPC port before it could show anything. It now boots alongside the main bundle and mounts its shell from state the main process already has.

gantt
  dateFormat X
  axisFormat %L
  section before
  entry → ready               :a1, 0, 134
  window shown                :a2, 134, 192
  main bundle + layers        :a3, 192, 484
  renderer boot               :a4, 484, 583
  wait for port + 4 RPCs      :a5, 583, 889
  section after
  entry → ready               :b1, 0, 133
  window shown + loadURL      :b2, 133, 186
  renderer boot (main idle, serving assets) :b3, 186, 281
  main bundle + layers        :b4, 314, 448
  compile + evaluate + hydrated render :b5, 281, 390
Loading
  • createEarlyWindow registers the oc:// handler, permissions, navigation policy and headers itself and calls loadURL in the same tick the window is created. The entry chunk stays Effect-free; the protocol handler reports through console until the logging layer installs a reporter.
  • The main bundle waits for the renderer's initial asset burst (HTML, one JS chunk, one CSS file) before evaluating, so a 100 ms module evaluation no longer sits between the renderer and its HTML. The renderer's initial module graph is one chunk (codeSplitting.groups on everything statically reachable from the entry) instead of 47 files served one main-thread turn each.
  • The window's argv carries firstLaunchPending and defaultServerUrl; the preload asks main for the global, default and window state namespaces before the page runs (from the SQLite file until the storage layer is up, then from the layer so later windows see queued writes). The first render is the hydrated one — tabs are in the first frame, no re-render, no second layout pass.
  • The renderer speaks Effect's RPC wire format over the MessagePort directly (ipc-client.ts), keeping Effect's runtime, Stream, Layer, the RPC client and its timer-based fiber scheduler out of the renderer's initial module graph. Main is unchanged (RpcServer).
  • Off the path to the port: the onboarding directory scan when settings already hold the answer, log cleanup, system certificates / proxy / net log (forked), crashReporter.start (~60 ms, spawns crashpad; now runs ~500 ms after the windows), async CLI resolve (now sync fs), eager theme palette resolution for both schemes, and a read-only background-service probe during main's idle window.
// renderer/api.ts — first read of a namespace the preload already fetched
storeItems: (name) =>
  seeded.then((snapshot) => {
    const item = snapshot.get(name)
    if (!item) return invoke("StorageItems", { name }).then(mutable)
    snapshot.delete(name)
    return item
  }),

Packaged startup bench (bun run bench:startup), Windows laptop display, warm service, 5 interleaved runs, medians in ms since spawn:

v2 this PR
renderer process starts 468 179
DOM interactive 582 263
shell in DOM (titlebar tab) 853 387
composer editable 853 613
first pixels on screen 573 622
screen settled 1132 945

Window → shell: 664 → 210 ms. First pixels land ~50 ms later than before but as the complete hydrated frame; on stock Electron that instant is set by GPU shader compilation, which the renderer now finishes ahead of (electron/electron#54113 removes that wait).

Note: the crash reporter now starts after the first window is up, so a crash in roughly the first second of a launch is not reported.

The first window loads its renderer the moment it exists instead of after every layer, the shell mounts from settings handed over in the window's argv instead of waiting for the IPC port, the initial module graph ships as one chunk, and main defers the crash reporter, certificates, log cleanup and the onboarding scan off the path to the port. Window to shell on the packaged startup bench drops from ~680 ms to ~450 ms.
… the renderer

The renderer speaks Effect's RPC wire format over the MessagePort directly: messages are structured clones, every payload is JSON-native or a Uint8Array, and the main process is trusted. That keeps Effect's runtime, Stream, Layer, RPC client and its timer-based fiber scheduler out of the renderer's initial module graph. Also adds a source-map attributed summary for renderer CPU profiles.
…render

The preload asks the main process for the global, default and window state namespaces before the page runs; the answer comes from the database file until the storage layer is up, then from the layer so later windows see queued writes. The first render is the hydrated one: tabs are in the first frame instead of popping in after a round trip and a second layout pass. Window to shell on the packaged startup bench: ~424 ms to ~234 ms.
@Hona
Hona requested a review from Brendonovich as a code owner September 19, 2026 20:46
Copilot AI lite review requested due to automatic review settings September 19, 2026 20:46

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 commented Sep 20, 2026

Copy link
Copy Markdown
Member Author

Split into a 7-PR stack, bottom at #50108 (stack #50115). Same final tree.

@Hona Hona closed this Sep 20, 2026
@Hona
Hona deleted the desktop-startup branch September 20, 2026 06:14
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