perf: memoize MemberBadge, CurrentTaskIndicator, MemberPresenceDot, ReplyQuoteBlock, GlobalTaskList

Prevent unnecessary re-renders on these frequently-rendered components
that appear in MemberCard rows, activity feeds, and the sidebar task list.
This commit is contained in:
Mike 2026-05-02 21:28:17 +05:00
parent 8b30930c04
commit e300d4cbd5
5 changed files with 821 additions and 804 deletions

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { confirm } from '@renderer/components/common/ConfirmDialog'; import { confirm } from '@renderer/components/common/ConfirmDialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
@ -173,7 +173,8 @@ function applyProjectFilter(tasks: GlobalTask[], projectPath: string | null): Gl
return tasks.filter((t) => t.projectPath && normalizePath(t.projectPath) === normalized); return tasks.filter((t) => t.projectPath && normalizePath(t.projectPath) === normalized);
} }
export const GlobalTaskList = ({ export const GlobalTaskList = memo(
({
hideHeader = false, hideHeader = false,
filters: externalFilters, filters: externalFilters,
onFiltersChange: externalOnFiltersChange, onFiltersChange: externalOnFiltersChange,
@ -209,7 +210,8 @@ export const GlobalTaskList = ({
const filters = externalFilters ?? internalFilters; const filters = externalFilters ?? internalFilters;
const setFilters = externalOnFiltersChange ?? setInternalFilters; const setFilters = externalOnFiltersChange ?? setInternalFilters;
const filtersPopoverOpen = externalFiltersPopoverOpen ?? internalFiltersPopoverOpen; const filtersPopoverOpen = externalFiltersPopoverOpen ?? internalFiltersPopoverOpen;
const setFiltersPopoverOpen = externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen; const setFiltersPopoverOpen =
externalOnFiltersPopoverOpenChange ?? setInternalFiltersPopoverOpen;
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [groupingMode, setGroupingModeState] = useState<TaskGroupingMode>(loadGroupingMode); const [groupingMode, setGroupingModeState] = useState<TaskGroupingMode>(loadGroupingMode);
const [sortMode, setSortModeState] = useState<TaskSortMode>(loadSortMode); const [sortMode, setSortModeState] = useState<TaskSortMode>(loadSortMode);
@ -416,7 +418,10 @@ export const GlobalTaskList = ({
); );
const projectVisibleCountByKey = useMemo( const projectVisibleCountByKey = useMemo(
() => () =>
syncProjectGroupVisibleCountByKey(projectRequestedVisibleCountByKey, projectGroupVisibility), syncProjectGroupVisibleCountByKey(
projectRequestedVisibleCountByKey,
projectGroupVisibility
),
[projectRequestedVisibleCountByKey, projectGroupVisibility] [projectRequestedVisibleCountByKey, projectGroupVisibility]
); );
@ -573,7 +578,9 @@ export const GlobalTaskList = ({
onClick={() => setGroupingMode(mode)} onClick={() => setGroupingMode(mode)}
className={cn( className={cn(
'rounded px-1.5 py-0.5 transition-colors', 'rounded px-1.5 py-0.5 transition-colors',
groupingMode === mode ? 'text-text' : 'text-text-muted hover:text-text-secondary' groupingMode === mode
? 'text-text'
: 'text-text-muted hover:text-text-secondary'
)} )}
> >
{label} {label}
@ -850,4 +857,5 @@ export const GlobalTaskList = ({
</div> </div>
</div> </div>
); );
}; }
);

View file

@ -1,4 +1,4 @@
import { useMemo } from 'react'; import { memo, useMemo } from 'react';
import { import {
getTeamColorSet, getTeamColorSet,
@ -37,7 +37,8 @@ interface MemberBadgeProps {
* When onClick is provided, both avatar and badge are clickable as one unit. * When onClick is provided, both avatar and badge are clickable as one unit.
* Wrapped in MemberHoverCard to show member info on hover. * Wrapped in MemberHoverCard to show member info on hover.
*/ */
export const MemberBadge = ({ export const MemberBadge = memo(
({
name, name,
color, color,
teamName, teamName,
@ -114,4 +115,5 @@ export const MemberBadge = ({
{content} {content}
</MemberHoverCard> </MemberHoverCard>
); );
}; }
);

View file

@ -1,4 +1,4 @@
import { useState } from 'react'; import { memo, useState } from 'react';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { MemberBadge } from '@renderer/components/team/MemberBadge';
@ -20,7 +20,8 @@ interface ReplyQuoteBlockProps {
/** Threshold (characters) above which the "more/less" toggle is shown. */ /** Threshold (characters) above which the "more/less" toggle is shown. */
const LONG_QUOTE_THRESHOLD = 200; const LONG_QUOTE_THRESHOLD = 200;
export const ReplyQuoteBlock = ({ export const ReplyQuoteBlock = memo(
({
reply, reply,
memberColor, memberColor,
bodyMaxHeight = 'max-h-56', bodyMaxHeight = 'max-h-56',
@ -76,4 +77,5 @@ export const ReplyQuoteBlock = ({
/> />
</div> </div>
); );
}; }
);

View file

@ -1,3 +1,5 @@
import { memo } from 'react';
import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2'; import { SyncedLoader2 } from '@renderer/components/ui/SyncedLoader2';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
@ -15,7 +17,8 @@ interface CurrentTaskIndicatorProps {
* Inline indicator showing a spinning loader + "working on" + task label button. * Inline indicator showing a spinning loader + "working on" + task label button.
* Shared between MemberCard and MemberHoverCard. * Shared between MemberCard and MemberHoverCard.
*/ */
export const CurrentTaskIndicator = ({ export const CurrentTaskIndicator = memo(
({
task, task,
borderColor, borderColor,
maxSubjectLength, maxSubjectLength,
@ -53,4 +56,5 @@ export const CurrentTaskIndicator = ({
</button> </button>
</div> </div>
); );
}; }
);

View file

@ -1,3 +1,5 @@
import { memo } from 'react';
import { useSyncedAnimationStyle } from '@renderer/hooks/useSyncedAnimationStyle'; import { useSyncedAnimationStyle } from '@renderer/hooks/useSyncedAnimationStyle';
import { cn } from '@renderer/lib/utils'; import { cn } from '@renderer/lib/utils';
@ -8,10 +10,8 @@ interface MemberPresenceDotProps {
label: string; label: string;
} }
export const MemberPresenceDot = ({ export const MemberPresenceDot = memo(
className, ({ className, label }: MemberPresenceDotProps): React.JSX.Element => {
label,
}: MemberPresenceDotProps): React.JSX.Element => {
const shouldSyncPulse = className?.includes('animate-pulse') === true; const shouldSyncPulse = className?.includes('animate-pulse') === true;
const syncedPulseStyle = useSyncedAnimationStyle(shouldSyncPulse, PULSE_DURATION_MS); const syncedPulseStyle = useSyncedAnimationStyle(shouldSyncPulse, PULSE_DURATION_MS);
@ -25,4 +25,5 @@ export const MemberPresenceDot = ({
aria-label={label} aria-label={label}
/> />
); );
}; }
);