From e702a45b5d35aeae8793ea2ce11aa61005251470 Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Sun, 19 Jul 2026 23:31:32 -0500 Subject: [PATCH] perf(desktop): idle-mount boot-hidden panes off the cold-start critical path (#67857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(desktop): idle-mount boot-hidden panes off the cold-start critical path The layout tree keeps a chrome-hidden pane's content MOUNTED behind display:none (so toggling back is instant) — but that means files, preview, review (Shiki diff) and logs all mount their real content during first paint even though none are visible at launch (fresh profile: no cwd, review off, no preview target, logs not in the default tree). First paint only needs sessions + workspace + statusbar; the rest is pure app-mount tax, the one cold-start lever that's actually in our code (Electron startup and the un-splittable bundle eval are not). Wrap those four pane renders in : mount on requestIdleCallback (2s timeout fallback), then stay mounted. Idle fires within a frame of first paint, so a hidden pane is warm before it can be revealed — zero UX change, the instant-toggle contract intact. Degrades to eager mount where rIC is absent (jsdom/tests), so no behavioral fork. * refactor(desktop): collapse the four idle-mount wrappers into one idle() helper --- apps/desktop/src/app/contrib/controller.tsx | 13 +++-- .../src/components/idle-mount.test.tsx | 50 +++++++++++++++++++ apps/desktop/src/components/idle-mount.tsx | 28 +++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/components/idle-mount.test.tsx create mode 100644 apps/desktop/src/components/idle-mount.tsx diff --git a/apps/desktop/src/app/contrib/controller.tsx b/apps/desktop/src/app/contrib/controller.tsx index cfa41938fdfa..5edd193dbca2 100644 --- a/apps/desktop/src/app/contrib/controller.tsx +++ b/apps/desktop/src/app/contrib/controller.tsx @@ -5,6 +5,7 @@ import type { CSSProperties, ReactElement, PointerEvent as ReactPointerEvent } f import { PREVIEW_RAIL_MAX_WIDTH, PREVIEW_RAIL_MIN_WIDTH } from '@/app/chat/right-rail' import { PALETTE_AREA, type PaletteContribution } from '@/app/command-palette/contrib' import { type StatusbarItem } from '@/app/shell/statusbar-controls' +import { IdleMount } from '@/components/idle-mount' import { toggleLayoutEditMode } from '@/components/pane-shell/edit-mode' import { allPaneIds, group, split } from '@/components/pane-shell/tree/model' import { LayoutTreeRoot } from '@/components/pane-shell/tree/renderer' @@ -90,6 +91,10 @@ import { ContribWiring, WiredPane } from './wiring' // ONE render identity for the workspace pane — syncWorkspaceTitle re-registers // the contribution (new title) and a fresh closure would remount the chat. const renderWorkspacePane = () => + +// Boot-hidden panes mount behind display:none (instant-toggle contract) — defer +// them to idle so they're off the first-paint path, warm before reveal. +const idle = (node: ReactElement) => {node} // The main tab carries the same session context menu as tile tabs (targets // the loaded primary session; no menu on a fresh draft). const wrapWorkspaceTab = (tab: ReactElement) => {tab} @@ -182,7 +187,7 @@ registry.registerMany([ minWidth: FILE_BROWSER_MIN_WIDTH, maxWidth: FILE_BROWSER_MAX_WIDTH }, - render: () => + render: () => idle() }, { id: 'preview', @@ -200,7 +205,7 @@ registry.registerMany([ minWidth: PREVIEW_RAIL_MIN_WIDTH, maxWidth: PREVIEW_RAIL_MAX_WIDTH }, - render: () => + render: () => idle() }, { id: 'review', @@ -216,7 +221,7 @@ registry.registerMany([ minWidth: FILE_BROWSER_MIN_WIDTH, maxWidth: FILE_BROWSER_MAX_WIDTH }, - render: () => + render: () => idle() }, { // Optional chrome — in NO default layout. Adoption stacks it with the @@ -227,7 +232,7 @@ registry.registerMany([ // revealOnPreset: the Quad layout places logs, so applying it turns the // logs pane on (like a ⌘K "Toggle logs") instead of leaving it collapsed. data: { placement: 'bottom', height: '20vh', minHeight: '7.5rem', maxHeight: '80vh', revealOnPreset: true }, - render: () => + render: () => idle() } ]) diff --git a/apps/desktop/src/components/idle-mount.test.tsx b/apps/desktop/src/components/idle-mount.test.tsx new file mode 100644 index 000000000000..db51c1578c83 --- /dev/null +++ b/apps/desktop/src/components/idle-mount.test.tsx @@ -0,0 +1,50 @@ +import { act, cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { IdleMount } from './idle-mount' + +afterEach(() => { + cleanup() + vi.unstubAllGlobals() +}) + +describe('IdleMount', () => { + it('mounts eagerly where requestIdleCallback is unavailable', () => { + vi.stubGlobal('requestIdleCallback', undefined) + + render( + + child + + ) + + expect(screen.getByText('child')).toBeTruthy() + }) + + it('defers the child to idle, then keeps it mounted', () => { + let fire: (() => void) | null = null + + const ric = vi.fn((cb: () => void) => { + fire = cb + + return 1 + }) + + vi.stubGlobal('requestIdleCallback', ric) + vi.stubGlobal('cancelIdleCallback', vi.fn()) + + render( + + child + + ) + + // Not on the first-paint path — nothing rendered until the browser is idle. + expect(screen.queryByText('child')).toBeNull() + expect(ric).toHaveBeenCalledOnce() + + act(() => fire?.()) + + expect(screen.getByText('child')).toBeTruthy() + }) +}) diff --git a/apps/desktop/src/components/idle-mount.tsx b/apps/desktop/src/components/idle-mount.tsx new file mode 100644 index 000000000000..9a15ce84b508 --- /dev/null +++ b/apps/desktop/src/components/idle-mount.tsx @@ -0,0 +1,28 @@ +import { type ReactNode, useEffect, useState } from 'react' + +/** + * Mounts `children` only once the browser goes idle (or `timeout` ms elapse), + * then keeps them mounted for good. Lifts non-critical, boot-hidden surfaces + * (display:none panes: files/preview/review/logs) off the first-paint critical + * path with ZERO visible change — idle fires within a frame of first paint, so + * a hidden pane is warm long before the user can reveal it, preserving the + * "toggle back is instant" contract while shrinking cold-start app-mount. + * + * Degrades to eager mount where requestIdleCallback is absent (jsdom/tests, + * older webviews), so there's no behavioral fork to reason about there. + */ +export function IdleMount({ children, timeout = 2000 }: { children: ReactNode; timeout?: number }) { + const [ready, setReady] = useState(typeof requestIdleCallback !== 'function') + + useEffect(() => { + if (ready) { + return undefined + } + + const id = requestIdleCallback(() => setReady(true), { timeout }) + + return () => cancelIdleCallback(id) + }, [ready, timeout]) + + return ready ? <>{children} : null +}