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 <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip committed 2026-04-08 15:04:50 -07:00
1 parent 96f72be88e
commit 67cda7f3cd
1 file changed
+33 -18
+33 -18
View File
@@ -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 (
<div className="space-y-4">
<h3 className="text-sm font-semibold">Timeline ({timeline.length + queuedComments.length})</h3>
const timelineSection = useMemo(
() => (
<TimelineList
timeline={timeline}
agentMap={agentMap}
@@ -761,6 +762,20 @@ export const CommentThread = memo(function CommentThread({
highlightCommentId={highlightCommentId}
feedbackTermsUrl={feedbackTermsUrl}
/>
),
[
timeline, agentMap, currentUserId, companyId, projectId,
feedbackVoteByTargetId, feedbackDataSharingPreference,
onVote, handleFeedbackVote, votingTargetId, highlightCommentId,
feedbackTermsUrl,
],
);
return (
<div className="space-y-4">
<h3 className="text-sm font-semibold">Timeline ({timeline.length + queuedComments.length})</h3>
{timelineSection}
{liveRunSlot}