perf(desktop): idle-mount boot-hidden panes off the cold-start critical path (#67857)

* 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 <IdleMount>: 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
This commit is contained in:
brooklyn! 2026-07-19 23:31:32 -05:00 committed by GitHub
parent 9c3ffcaae3
commit e702a45b5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 87 additions and 4 deletions

View file

@ -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 = () => <WiredPane part="chatRoutes" />
// 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) => <IdleMount>{node}</IdleMount>
// 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) => <WorkspaceTabMenu>{tab}</WorkspaceTabMenu>
@ -182,7 +187,7 @@ registry.registerMany([
minWidth: FILE_BROWSER_MIN_WIDTH,
maxWidth: FILE_BROWSER_MAX_WIDTH
},
render: () => <FilesPane />
render: () => idle(<FilesPane />)
},
{
id: 'preview',
@ -200,7 +205,7 @@ registry.registerMany([
minWidth: PREVIEW_RAIL_MIN_WIDTH,
maxWidth: PREVIEW_RAIL_MAX_WIDTH
},
render: () => <PreviewRailPane />
render: () => idle(<PreviewRailPane />)
},
{
id: 'review',
@ -216,7 +221,7 @@ registry.registerMany([
minWidth: FILE_BROWSER_MIN_WIDTH,
maxWidth: FILE_BROWSER_MAX_WIDTH
},
render: () => <ReviewPaneContent />
render: () => idle(<ReviewPaneContent />)
},
{
// 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: () => <LogsPane />
render: () => idle(<LogsPane />)
}
])

View file

@ -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(
<IdleMount>
<span>child</span>
</IdleMount>
)
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(
<IdleMount>
<span>child</span>
</IdleMount>
)
// 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()
})
})

View file

@ -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
}