From 67cda7f3cd0db0502a0e60d8a5ad563dbde62dd4 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 8 Apr 2026 15:04:50 -0700 Subject: [PATCH] Memoize TimelineList in CommentThread to prevent re-render on typing The body state (composer text) was causing the entire CommentThread to re-render on every keystroke, including the TimelineList with 143+ items. This was the primary cause of typing lag (110ms average per keystroke, 60 long tasks totaling 3.7s of blocking). Wrap TimelineList in useMemo so it only re-renders when timeline data actually changes. Convert handleFeedbackVote to useCallback so it doesn't defeat the memoization. Before: 110ms avg, 181ms P95, 60 long tasks After: 21ms avg, 35ms P95, 5 long tasks Co-Authored-By: Paperclip --- ui/src/components/CommentThread.tsx | 51 +++++++++++++++++++---------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/ui/src/components/CommentThread.tsx b/ui/src/components/CommentThread.tsx index 4d2bdc87111..020b2c2cdb6 100644 --- a/ui/src/components/CommentThread.tsx +++ b/ui/src/components/CommentThread.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; import { Link, useLocation } from "react-router-dom"; import type { Agent, @@ -728,26 +728,27 @@ export const CommentThread = memo(function CommentThread({ } } - async function handleFeedbackVote( - commentId: string, - vote: FeedbackVoteValue, - options?: { allowSharing?: boolean; reason?: string }, - ) { - if (!onVote) return; - setVotingTargetId(commentId); - try { - await onVote(commentId, vote, options); - } finally { - setVotingTargetId(null); - } - } + const handleFeedbackVote = useCallback( + async ( + commentId: string, + vote: FeedbackVoteValue, + options?: { allowSharing?: boolean; reason?: string }, + ) => { + if (!onVote) return; + setVotingTargetId(commentId); + try { + await onVote(commentId, vote, options); + } finally { + setVotingTargetId(null); + } + }, + [onVote], + ); const canSubmit = !submitting && !!body.trim(); - return ( -
-

Timeline ({timeline.length + queuedComments.length})

- + const timelineSection = useMemo( + () => ( + ), + [ + timeline, agentMap, currentUserId, companyId, projectId, + feedbackVoteByTargetId, feedbackDataSharingPreference, + onVote, handleFeedbackVote, votingTargetId, highlightCommentId, + feedbackTermsUrl, + ], + ); + + return ( +
+

Timeline ({timeline.length + queuedComments.length})

+ + {timelineSection} {liveRunSlot}