perf(renderer): stable callbacks and lazy-load large dialogs
Move toggleSidebarSessionSelection into SessionItem's own store subscription, eliminating the inline arrow function prop that was breaking its memo on every sidebar render. Lazy-load LaunchTeamDialog (2918L) and CreateTeamDialog (2208L) in all four host components (TeamDetailView, TeamListView, SchedulesView, ScheduleSection). These dialogs are never needed at initial mount — they only open on user action. Deferring their parse/compile saves ~175KB of JS from the initial render path.
This commit is contained in:
parent
fa38b90f9c
commit
2bda324e1a
6 changed files with 105 additions and 83 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Button } from '@renderer/components/ui/button';
|
||||
import { Input } from '@renderer/components/ui/input';
|
||||
|
|
@ -24,8 +24,11 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { LaunchTeamDialog } from '../team/dialogs/LaunchTeamDialog';
|
||||
import { ScheduleRunLogDialog } from '../team/schedule/ScheduleRunLogDialog';
|
||||
|
||||
const LaunchTeamDialog = lazy(() =>
|
||||
import('../team/dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
|
||||
);
|
||||
import { ScheduleRunRow } from '../team/schedule/ScheduleRunRow';
|
||||
import { ScheduleStatusBadge } from '../team/schedule/ScheduleStatusBadge';
|
||||
|
||||
|
|
@ -562,13 +565,15 @@ export const SchedulesView = (): React.JSX.Element => {
|
|||
</div>
|
||||
|
||||
{/* Create/Edit Dialog */}
|
||||
<LaunchTeamDialog
|
||||
mode="schedule"
|
||||
open={dialogOpen}
|
||||
teamName={editingSchedule?.teamName}
|
||||
schedule={editingSchedule}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<LaunchTeamDialog
|
||||
mode="schedule"
|
||||
open={dialogOpen}
|
||||
teamName={editingSchedule?.teamName}
|
||||
schedule={editingSchedule}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -202,7 +202,6 @@ export const DateGroupedSessions = memo((): React.JSX.Element => {
|
|||
toggleShowHiddenSessions,
|
||||
sidebarSelectedSessionIds,
|
||||
sidebarMultiSelectActive,
|
||||
toggleSidebarSessionSelection,
|
||||
clearSidebarSelection,
|
||||
toggleSidebarMultiSelect,
|
||||
hideMultipleSessions,
|
||||
|
|
@ -239,7 +238,6 @@ export const DateGroupedSessions = memo((): React.JSX.Element => {
|
|||
toggleShowHiddenSessions: s.toggleShowHiddenSessions,
|
||||
sidebarSelectedSessionIds: s.sidebarSelectedSessionIds,
|
||||
sidebarMultiSelectActive: s.sidebarMultiSelectActive,
|
||||
toggleSidebarSessionSelection: s.toggleSidebarSessionSelection,
|
||||
clearSidebarSelection: s.clearSidebarSelection,
|
||||
toggleSidebarMultiSelect: s.toggleSidebarMultiSelect,
|
||||
hideMultipleSessions: s.hideMultipleSessions,
|
||||
|
|
@ -1104,7 +1102,6 @@ export const DateGroupedSessions = memo((): React.JSX.Element => {
|
|||
isHidden={item.isHidden}
|
||||
multiSelectActive={sidebarMultiSelectActive}
|
||||
isSelected={selectedSet.has(item.session.id)}
|
||||
onToggleSelect={() => toggleSidebarSessionSelection(item.session.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ interface SessionItemProps {
|
|||
isHidden?: boolean;
|
||||
multiSelectActive?: boolean;
|
||||
isSelected?: boolean;
|
||||
onToggleSelect?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -164,7 +163,6 @@ export const SessionItem = memo(
|
|||
isHidden,
|
||||
multiSelectActive,
|
||||
isSelected,
|
||||
onToggleSelect,
|
||||
}: Readonly<SessionItemProps>): React.JSX.Element => {
|
||||
const {
|
||||
openTab,
|
||||
|
|
@ -174,6 +172,7 @@ export const SessionItem = memo(
|
|||
splitPane,
|
||||
togglePinSession,
|
||||
toggleHideSession,
|
||||
toggleSidebarSessionSelection,
|
||||
} = useStore(
|
||||
useShallow((s) => ({
|
||||
openTab: s.openTab,
|
||||
|
|
@ -183,6 +182,7 @@ export const SessionItem = memo(
|
|||
splitPane: s.splitPane,
|
||||
togglePinSession: s.togglePinSession,
|
||||
toggleHideSession: s.toggleHideSession,
|
||||
toggleSidebarSessionSelection: s.toggleSidebarSessionSelection,
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
@ -192,8 +192,8 @@ export const SessionItem = memo(
|
|||
if (!activeProjectId) return;
|
||||
|
||||
// In multi-select mode, clicks toggle selection
|
||||
if (multiSelectActive && onToggleSelect) {
|
||||
onToggleSelect();
|
||||
if (multiSelectActive) {
|
||||
toggleSidebarSessionSelection(session.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -291,7 +291,7 @@ export const SessionItem = memo(
|
|||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected ?? false}
|
||||
onChange={() => onToggleSelect?.()}
|
||||
onChange={() => toggleSidebarSessionSelection(session.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="size-3.5 shrink-0 accent-blue-500"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ import { useShallow } from 'zustand/react/shallow';
|
|||
import { AddMemberDialog } from './dialogs/AddMemberDialog';
|
||||
import { CreateTaskDialog } from './dialogs/CreateTaskDialog';
|
||||
import { EditTeamDialog } from './dialogs/EditTeamDialog';
|
||||
import { LaunchTeamDialog, type TeamLaunchDialogMode } from './dialogs/LaunchTeamDialog';
|
||||
import type { TeamLaunchDialogMode } from './dialogs/LaunchTeamDialog';
|
||||
import { ReviewDialog } from './dialogs/ReviewDialog';
|
||||
import { SendMessageDialog } from './dialogs/SendMessageDialog';
|
||||
import { TaskDetailDialog } from './dialogs/TaskDetailDialog';
|
||||
|
|
@ -96,6 +96,9 @@ import type { AddMemberEntry } from './dialogs/AddMemberDialog';
|
|||
import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode';
|
||||
import type { ComponentProps, CSSProperties } from 'react';
|
||||
|
||||
const LaunchTeamDialog = lazy(() =>
|
||||
import('./dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
|
||||
);
|
||||
const ProjectEditorOverlay = lazy(() =>
|
||||
import('./editor/ProjectEditorOverlay').then((m) => ({ default: m.ProjectEditorOverlay }))
|
||||
);
|
||||
|
|
@ -2176,18 +2179,20 @@ export const TeamDetailView = memo(
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LaunchTeamDialog
|
||||
mode={launchDialogState.mode}
|
||||
open={launchDialogOpen}
|
||||
teamName={teamName}
|
||||
members={[]}
|
||||
defaultProjectPath={draftTeamSummary?.projectPath}
|
||||
provisioningError={provisioningError}
|
||||
clearProvisioningError={clearProvisioningError}
|
||||
onClose={closeLaunchDialog}
|
||||
onLaunch={handleLaunchDialogSubmit}
|
||||
onRelaunch={handleRelaunchDialogSubmit}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<LaunchTeamDialog
|
||||
mode={launchDialogState.mode}
|
||||
open={launchDialogOpen}
|
||||
teamName={teamName}
|
||||
members={[]}
|
||||
defaultProjectPath={draftTeamSummary?.projectPath}
|
||||
provisioningError={provisioningError}
|
||||
clearProvisioningError={clearProvisioningError}
|
||||
onClose={closeLaunchDialog}
|
||||
onLaunch={handleLaunchDialogSubmit}
|
||||
onRelaunch={handleRelaunchDialogSubmit}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2976,19 +2981,21 @@ export const TeamDetailView = memo(
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<LaunchTeamDialog
|
||||
mode={launchDialogState.mode}
|
||||
open={launchDialogOpen}
|
||||
teamName={teamName}
|
||||
members={membersWithLiveBranches}
|
||||
defaultProjectPath={data.config.projectPath}
|
||||
provisioningError={provisioningError}
|
||||
clearProvisioningError={clearProvisioningError}
|
||||
activeTeams={activeTeamsForLaunch}
|
||||
onClose={closeLaunchDialog}
|
||||
onLaunch={handleLaunchDialogSubmit}
|
||||
onRelaunch={handleRelaunchDialogSubmit}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<LaunchTeamDialog
|
||||
mode={launchDialogState.mode}
|
||||
open={launchDialogOpen}
|
||||
teamName={teamName}
|
||||
members={membersWithLiveBranches}
|
||||
defaultProjectPath={data.config.projectPath}
|
||||
provisioningError={provisioningError}
|
||||
clearProvisioningError={clearProvisioningError}
|
||||
activeTeams={activeTeamsForLaunch}
|
||||
onClose={closeLaunchDialog}
|
||||
onLaunch={handleLaunchDialogSubmit}
|
||||
onRelaunch={handleRelaunchDialogSubmit}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<SendMessageDialog
|
||||
open={sendDialogOpen}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { recordRecentProjectOpenPaths } from '@features/recent-projects/renderer';
|
||||
import { api, isElectronMode } from '@renderer/api';
|
||||
|
|
@ -45,8 +45,7 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { CreateTeamDialog } from './dialogs/CreateTeamDialog';
|
||||
import { LaunchTeamDialog } from './dialogs/LaunchTeamDialog';
|
||||
import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog';
|
||||
import { TeamEmptyState } from './TeamEmptyState';
|
||||
import { EMPTY_TEAM_FILTER, TeamListFilterPopover } from './TeamListFilterPopover';
|
||||
import {
|
||||
|
|
@ -55,7 +54,13 @@ import {
|
|||
teamMatchesProjectSelection,
|
||||
} from './teamProjectSelection';
|
||||
|
||||
import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog';
|
||||
const CreateTeamDialog = lazy(() =>
|
||||
import('./dialogs/CreateTeamDialog').then((m) => ({ default: m.CreateTeamDialog }))
|
||||
);
|
||||
const LaunchTeamDialog = lazy(() =>
|
||||
import('./dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
|
||||
);
|
||||
|
||||
import type { TeamListFilterState } from './TeamListFilterPopover';
|
||||
import type { TeamStatus } from '@renderer/utils/teamListStatus';
|
||||
import type {
|
||||
|
|
@ -732,35 +737,39 @@ export const TeamListView = memo((): React.JSX.Element => {
|
|||
}
|
||||
|
||||
const createDialogElement = (
|
||||
<CreateTeamDialog
|
||||
open={showCreateDialog}
|
||||
canCreate={canCreate}
|
||||
provisioningErrorsByTeam={provisioningErrorByTeam}
|
||||
clearProvisioningError={clearProvisioningError}
|
||||
existingTeamNames={teams.map((t) => t.teamName)}
|
||||
provisioningTeamNames={provisioningTeamNames}
|
||||
activeTeams={activeTeams}
|
||||
initialData={copyData ?? undefined}
|
||||
defaultProjectPath={currentProjectPath}
|
||||
onClose={handleCreateDialogClose}
|
||||
onCreate={handleCreateSubmit}
|
||||
onOpenTeam={openTeamTab}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<CreateTeamDialog
|
||||
open={showCreateDialog}
|
||||
canCreate={canCreate}
|
||||
provisioningErrorsByTeam={provisioningErrorByTeam}
|
||||
clearProvisioningError={clearProvisioningError}
|
||||
existingTeamNames={teams.map((t) => t.teamName)}
|
||||
provisioningTeamNames={provisioningTeamNames}
|
||||
activeTeams={activeTeams}
|
||||
initialData={copyData ?? undefined}
|
||||
defaultProjectPath={currentProjectPath}
|
||||
onClose={handleCreateDialogClose}
|
||||
onCreate={handleCreateSubmit}
|
||||
onOpenTeam={openTeamTab}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const launchDialogElement = (
|
||||
<LaunchTeamDialog
|
||||
mode="launch"
|
||||
open={launchDialogOpen}
|
||||
teamName={launchDialogTeamName}
|
||||
members={launchDialogMembers}
|
||||
defaultProjectPath={launchDialogDefaultPath}
|
||||
provisioningError={provisioningErrorByTeam[launchDialogTeamName] ?? null}
|
||||
clearProvisioningError={clearProvisioningError}
|
||||
activeTeams={activeTeams}
|
||||
onClose={() => setLaunchDialogOpen(false)}
|
||||
onLaunch={handleLaunchSubmit}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<LaunchTeamDialog
|
||||
mode="launch"
|
||||
open={launchDialogOpen}
|
||||
teamName={launchDialogTeamName}
|
||||
members={launchDialogMembers}
|
||||
defaultProjectPath={launchDialogDefaultPath}
|
||||
provisioningError={provisioningErrorByTeam[launchDialogTeamName] ?? null}
|
||||
clearProvisioningError={clearProvisioningError}
|
||||
activeTeams={activeTeams}
|
||||
onClose={() => setLaunchDialogOpen(false)}
|
||||
onLaunch={handleLaunchSubmit}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const renderHeader = (): React.JSX.Element => (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Button } from '@renderer/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
|
||||
|
|
@ -18,9 +18,11 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { LaunchTeamDialog } from '../dialogs/LaunchTeamDialog';
|
||||
|
||||
import { ScheduleEmptyState } from './ScheduleEmptyState';
|
||||
|
||||
const LaunchTeamDialog = lazy(() =>
|
||||
import('../dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog }))
|
||||
);
|
||||
import { ScheduleRunLogDialog } from './ScheduleRunLogDialog';
|
||||
import { ScheduleRunRow } from './ScheduleRunRow';
|
||||
import { ScheduleStatusBadge } from './ScheduleStatusBadge';
|
||||
|
|
@ -305,13 +307,15 @@ export const ScheduleSection = ({ teamName }: ScheduleSectionProps): React.JSX.E
|
|||
)}
|
||||
|
||||
{/* Create/Edit Dialog */}
|
||||
<LaunchTeamDialog
|
||||
mode="schedule"
|
||||
open={dialogOpen}
|
||||
teamName={teamName}
|
||||
schedule={editingSchedule}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<LaunchTeamDialog
|
||||
mode="schedule"
|
||||
open={dialogOpen}
|
||||
teamName={teamName}
|
||||
schedule={editingSchedule}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue