History

Avoid timeline timer rerender commits (#2580)

cursor[bot] committed 6 months ago

Commit 2ba58071 parent c27109c1 file · +47 −13Browse files

Unified · Split

apps/web/src/components/chat/MessagesTimeline.tsx+47−13
@@ -520,19 +520,27 @@ function WorkingTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "workin · 519 unmodified lines
520520 }
521521
522522 // ---------------------------------------------------------------------------
523−// Self-ticking components — bypass LegendList memoisation entirely.
524−// Each owns a `nowMs` state value consumed in the render output so the
525−// React Compiler cannot elide the re-render as a no-op.
523+// Self-ticking labels — update their own text nodes so elapsed-time display
524+// does not create a React commit every second while a response is streaming.
526525 // ---------------------------------------------------------------------------
527526
528527 /** Live "Working for Xs" label. */
529528 function WorkingTimer({ createdAt }: { createdAt: string }) {
530− const [nowMs, setNowMs] = useState(() => Date.now());
529+ const textRef = useRef<HTMLSpanElement>(null);
530+ const initialText = formatWorkingTimerNow(createdAt);
531+
531532 useEffect(() => {
532− const id = setInterval(() => setNowMs(Date.now()), 1000);
533+ const updateText = () => {
534+ if (textRef.current) {
535+ textRef.current.textContent = formatWorkingTimerNow(createdAt);
536+ }
537+ };
538+ updateText();
539+ const id = setInterval(updateText, 1000);
533540 return () => clearInterval(id);
534541 }, [createdAt]);
535− return <>{formatWorkingTimer(createdAt, new Date(nowMs).toISOString()) ?? "0s"}</>;
542+
543+ return <span ref={textRef}>{initialText}</span>;
536544 }
537545
538546 /** Live timestamp + elapsed duration for a streaming assistant message. */
@@ -545,15 +553,28 @@ function LiveMessageMeta({ · 6 unmodified lines
545553 durationStart: string | null | undefined;
546554 timestampFormat: TimestampFormat;
547555 }) {
548− const [nowMs, setNowMs] = useState(() => Date.now());
556+ const textRef = useRef<HTMLSpanElement>(null);
557+ const initialText = formatLiveMessageMetaNow(createdAt, durationStart, timestampFormat);
558+
549559 useEffect(() => {
550− const id = setInterval(() => setNowMs(Date.now()), 1000);
560+ const updateText = () => {
561+ if (textRef.current) {
562+ textRef.current.textContent = formatLiveMessageMetaNow(
563+ createdAt,
564+ durationStart,
565+ timestampFormat,
566+ );
567+ }
568+ };
569+ updateText();
570+ if (!durationStart) {
571+ return;
572+ }
573+ const id = setInterval(updateText, 1000);
551574 return () => clearInterval(id);
552− }, [durationStart]);
553− const elapsed = durationStart
554− ? formatElapsed(durationStart, new Date(nowMs).toISOString())
555− : null;
556− return <>{formatMessageMeta(createdAt, elapsed, timestampFormat)}</>;
575+ }, [createdAt, durationStart, timestampFormat]);
576+
577+ return <span ref={textRef}>{initialText}</span>;
557578 }
558579
559580 // ---------------------------------------------------------------------------
@@ -860,6 +881,19 @@ function formatWorkingTimer(startIso: string, endIso: string): string | null { · 300 unmodified lines
860881 return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
861882 }
862883
884+function formatWorkingTimerNow(startIso: string): string {
885+ return formatWorkingTimer(startIso, new Date().toISOString()) ?? "0s";
886+}
887+
888+function formatLiveMessageMetaNow(
889+ createdAt: string,
890+ durationStart: string | null | undefined,
891+ timestampFormat: TimestampFormat,
892+): string {
893+ const elapsed = durationStart ? formatElapsed(durationStart, new Date().toISOString()) : null;
894+ return formatMessageMeta(createdAt, elapsed, timestampFormat);
895+}
896+
863897 function formatMessageMeta(
864898 createdAt: string,
865899 duration: string | null,