perf(renderer): wrap heavy view components in React.memo
TeamDetailView (3166L), TeamListView (1180L), DateGroupedSessions (1117L), and MarkdownViewer (1198L) were re-rendering on every parent render cycle. Wrapping them in memo() prevents cascading re-renders when their props and store subscriptions have not changed, targeting VSCode-level UI responsiveness.
This commit is contained in:
parent
7609c548c5
commit
f764af17d8
4 changed files with 2344 additions and 2314 deletions
|
|
@ -946,7 +946,8 @@ export const CompactMarkdownPreview: React.FC<CompactMarkdownPreviewProps> = Rea
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
|
||||||
|
({
|
||||||
content,
|
content,
|
||||||
maxHeight = 'max-h-96',
|
maxHeight = 'max-h-96',
|
||||||
className = '',
|
className = '',
|
||||||
|
|
@ -958,7 +959,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
||||||
baseDir,
|
baseDir,
|
||||||
teamColorByName: providedTeamColorByName,
|
teamColorByName: providedTeamColorByName,
|
||||||
onTeamClick: providedOnTeamClick,
|
onTeamClick: providedOnTeamClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [showRaw, setShowRaw] = React.useState(false);
|
const [showRaw, setShowRaw] = React.useState(false);
|
||||||
const [rawLimit, setRawLimit] = React.useState(LARGE_PREVIEW_CHARS);
|
const [rawLimit, setRawLimit] = React.useState(LARGE_PREVIEW_CHARS);
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
|
|
@ -1062,8 +1063,8 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
||||||
|
|
||||||
{isTooLarge && (
|
{isTooLarge && (
|
||||||
<div className="px-3 pb-2 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
|
<div className="px-3 pb-2 text-[11px]" style={{ color: COLOR_TEXT_MUTED }}>
|
||||||
Content is very large ({content.length.toLocaleString()} chars). Showing raw preview to
|
Content is very large ({content.length.toLocaleString()} chars). Showing raw preview
|
||||||
keep the UI responsive.
|
to keep the UI responsive.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1195,4 +1196,5 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* Supports multi-select with bulk actions and hidden session filtering.
|
* Supports multi-select with bulk actions and hidden session filtering.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { recordRecentProjectOpenPaths } from '@features/recent-projects/renderer';
|
import { recordRecentProjectOpenPaths } from '@features/recent-projects/renderer';
|
||||||
|
|
@ -184,7 +184,7 @@ function matchesSessionSearch(session: Session, query: string): boolean {
|
||||||
return haystack.includes(query);
|
return haystack.includes(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DateGroupedSessions = (): React.JSX.Element => {
|
export const DateGroupedSessions = memo((): React.JSX.Element => {
|
||||||
const {
|
const {
|
||||||
sessions,
|
sessions,
|
||||||
selectedSessionId,
|
selectedSessionId,
|
||||||
|
|
@ -1114,4 +1114,4 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
|
||||||
|
|
@ -947,10 +947,8 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TeamDetailView = ({
|
export const TeamDetailView = memo(
|
||||||
teamName,
|
({ teamName, isPaneFocused = false }: TeamDetailViewProps): React.JSX.Element => {
|
||||||
isPaneFocused = false,
|
|
||||||
}: TeamDetailViewProps): React.JSX.Element => {
|
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
const [requestChangesTaskId, setRequestChangesTaskId] = useState<string | null>(null);
|
const [requestChangesTaskId, setRequestChangesTaskId] = useState<string | null>(null);
|
||||||
const [selectedTask, setSelectedTask] = useState<TeamTaskWithKanban | null>(null);
|
const [selectedTask, setSelectedTask] = useState<TeamTaskWithKanban | null>(null);
|
||||||
|
|
@ -959,8 +957,8 @@ export const TeamDetailView = ({
|
||||||
initialTab?: MemberDetailTab;
|
initialTab?: MemberDetailTab;
|
||||||
initialActivityFilter?: MemberActivityFilter;
|
initialActivityFilter?: MemberActivityFilter;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [pendingRepliesByMember, setPendingRepliesByMember] = useState<Record<string, number>>(() =>
|
const [pendingRepliesByMember, setPendingRepliesByMember] = useState<Record<string, number>>(
|
||||||
getTeamPendingRepliesState(teamName)
|
() => getTeamPendingRepliesState(teamName)
|
||||||
);
|
);
|
||||||
const [createTaskDialog, setCreateTaskDialog] = useState<CreateTaskDialogState>({
|
const [createTaskDialog, setCreateTaskDialog] = useState<CreateTaskDialogState>({
|
||||||
open: false,
|
open: false,
|
||||||
|
|
@ -1192,7 +1190,9 @@ export const TeamDetailView = ({
|
||||||
const [stoppingTeam, setStoppingTeam] = useState(false);
|
const [stoppingTeam, setStoppingTeam] = useState(false);
|
||||||
const [trashOpen, setTrashOpen] = useState(false);
|
const [trashOpen, setTrashOpen] = useState(false);
|
||||||
const [sendDialogRecipient, setSendDialogRecipient] = useState<string | undefined>(undefined);
|
const [sendDialogRecipient, setSendDialogRecipient] = useState<string | undefined>(undefined);
|
||||||
const [sendDialogDefaultText, setSendDialogDefaultText] = useState<string | undefined>(undefined);
|
const [sendDialogDefaultText, setSendDialogDefaultText] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
const [sendDialogDefaultChip, setSendDialogDefaultChip] = useState<InlineChip | undefined>(
|
const [sendDialogDefaultChip, setSendDialogDefaultChip] = useState<InlineChip | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
@ -1353,7 +1353,8 @@ export const TeamDetailView = ({
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
side: 'left',
|
side: 'left',
|
||||||
});
|
});
|
||||||
const { isResizing: isLogsPanelResizing, handleProps: logsPanelHandleProps } = useResizablePanel({
|
const { isResizing: isLogsPanelResizing, handleProps: logsPanelHandleProps } =
|
||||||
|
useResizablePanel({
|
||||||
height: sidebarLogsHeight,
|
height: sidebarLogsHeight,
|
||||||
onHeightChange: setSidebarLogsHeight,
|
onHeightChange: setSidebarLogsHeight,
|
||||||
minHeight: 120,
|
minHeight: 120,
|
||||||
|
|
@ -1703,11 +1704,17 @@ export const TeamDetailView = ({
|
||||||
}, [activeMembers, data?.config.members, launchParams?.providerId]);
|
}, [activeMembers, data?.config.members, launchParams?.providerId]);
|
||||||
const shouldShowLeadContextUi = canShowLeadContextUi(leadProviderId);
|
const shouldShowLeadContextUi = canShowLeadContextUi(leadProviderId);
|
||||||
|
|
||||||
const taskMap = useMemo(() => new Map((data?.tasks ?? []).map((t) => [t.id, t])), [data?.tasks]);
|
const taskMap = useMemo(
|
||||||
|
() => new Map((data?.tasks ?? []).map((t) => [t.id, t])),
|
||||||
|
[data?.tasks]
|
||||||
|
);
|
||||||
const taskMapRef = useRef(taskMap);
|
const taskMapRef = useRef(taskMap);
|
||||||
taskMapRef.current = taskMap;
|
taskMapRef.current = taskMap;
|
||||||
|
|
||||||
const memberTaskCounts = useMemo(() => buildTaskCountsByOwner(data?.tasks ?? []), [data?.tasks]);
|
const memberTaskCounts = useMemo(
|
||||||
|
() => buildTaskCountsByOwner(data?.tasks ?? []),
|
||||||
|
[data?.tasks]
|
||||||
|
);
|
||||||
|
|
||||||
const openCreateTaskDialog = useCallback(
|
const openCreateTaskDialog = useCallback(
|
||||||
(subject = '', description = '', owner = '', startImmediately?: boolean): void => {
|
(subject = '', description = '', owner = '', startImmediately?: boolean): void => {
|
||||||
|
|
@ -2011,7 +2018,13 @@ export const TeamDetailView = ({
|
||||||
startImmediately,
|
startImmediately,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (prompt && owner && data?.isAlive && !isTeamProvisioning && startImmediately !== false) {
|
if (
|
||||||
|
prompt &&
|
||||||
|
owner &&
|
||||||
|
data?.isAlive &&
|
||||||
|
!isTeamProvisioning &&
|
||||||
|
startImmediately !== false
|
||||||
|
) {
|
||||||
const msg = `New task assigned to ${owner}: "${subject}". Instructions:\n${prompt}`;
|
const msg = `New task assigned to ${owner}: "${subject}". Instructions:\n${prompt}`;
|
||||||
try {
|
try {
|
||||||
await api.teams.processSend(teamName, msg);
|
await api.teams.processSend(teamName, msg);
|
||||||
|
|
@ -2351,7 +2364,10 @@ export const TeamDetailView = ({
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-0.5">
|
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-0.5">
|
||||||
{data.config.projectPath && (
|
{data.config.projectPath && (
|
||||||
<span className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]">
|
<span className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]">
|
||||||
<FolderOpen size={11} className="shrink-0 text-[var(--color-text-muted)]" />
|
<FolderOpen
|
||||||
|
size={11}
|
||||||
|
className="shrink-0 text-[var(--color-text-muted)]"
|
||||||
|
/>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="max-w-60 truncate font-mono">
|
<span className="max-w-60 truncate font-mono">
|
||||||
|
|
@ -2386,7 +2402,10 @@ export const TeamDetailView = ({
|
||||||
className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]"
|
className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]"
|
||||||
title={leadBranch}
|
title={leadBranch}
|
||||||
>
|
>
|
||||||
<GitBranch size={11} className="shrink-0 text-[var(--color-text-muted)]" />
|
<GitBranch
|
||||||
|
size={11}
|
||||||
|
className="shrink-0 text-[var(--color-text-muted)]"
|
||||||
|
/>
|
||||||
<span className="max-w-32 truncate">{leadBranch}</span>
|
<span className="max-w-32 truncate">{leadBranch}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -2415,7 +2434,9 @@ export const TeamDetailView = ({
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const currentPath = data.config.projectPath;
|
const currentPath = data.config.projectPath;
|
||||||
const history = data.config.projectPathHistory?.filter((p) => p !== currentPath);
|
const history = data.config.projectPathHistory?.filter(
|
||||||
|
(p) => p !== currentPath
|
||||||
|
);
|
||||||
if (!history || history.length === 0) return null;
|
if (!history || history.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -2508,7 +2529,9 @@ export const TeamDetailView = ({
|
||||||
sessionsError={sessionsError}
|
sessionsError={sessionsError}
|
||||||
leadSessionId={data.config.leadSessionId}
|
leadSessionId={data.config.leadSessionId}
|
||||||
selectedSessionId={kanbanFilter.sessionId}
|
selectedSessionId={kanbanFilter.sessionId}
|
||||||
onSelectSession={(id) => setKanbanFilter((prev) => ({ ...prev, sessionId: id }))}
|
onSelectSession={(id) =>
|
||||||
|
setKanbanFilter((prev) => ({ ...prev, sessionId: id }))
|
||||||
|
}
|
||||||
projectPath={data.config.projectPath}
|
projectPath={data.config.projectPath}
|
||||||
/>
|
/>
|
||||||
</CollapsibleTeamSection>
|
</CollapsibleTeamSection>
|
||||||
|
|
@ -2910,7 +2933,11 @@ export const TeamDetailView = ({
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" size="sm" onClick={() => setRemoveMemberConfirm(null)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setRemoveMemberConfirm(null)}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -2934,8 +2961,8 @@ export const TeamDetailView = ({
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Delete team</DialogTitle>
|
<DialogTitle>Delete team</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Delete team “{data.config.name}”? This action is irreversible. All
|
Delete team “{data.config.name}”? This action is irreversible.
|
||||||
team data and tasks will be deleted.
|
All team data and tasks will be deleted.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|
@ -3163,4 +3190,5 @@ export const TeamDetailView = ({
|
||||||
{renderBody()}
|
{renderBody()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { recordRecentProjectOpenPaths } from '@features/recent-projects/renderer';
|
import { recordRecentProjectOpenPaths } from '@features/recent-projects/renderer';
|
||||||
import { api, isElectronMode } from '@renderer/api';
|
import { api, isElectronMode } from '@renderer/api';
|
||||||
|
|
@ -233,7 +233,7 @@ const StatusBadge = ({ status }: { status: TeamStatus }): React.JSX.Element => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TeamListView = (): React.JSX.Element => {
|
export const TeamListView = memo((): React.JSX.Element => {
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
const electronMode = isElectronMode();
|
const electronMode = isElectronMode();
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
|
|
@ -1177,4 +1177,4 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue