mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
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:
parent
9c3ffcaae3
commit
e702a45b5d
3 changed files with 87 additions and 4 deletions
|
|
@ -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 />)
|
||||
}
|
||||
])
|
||||
|
||||
|
|
|
|||
50
apps/desktop/src/components/idle-mount.test.tsx
Normal file
50
apps/desktop/src/components/idle-mount.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
28
apps/desktop/src/components/idle-mount.tsx
Normal file
28
apps/desktop/src/components/idle-mount.tsx
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue