fix: isolate timer updates and optimize reasoning block rendering

- Extract ElapsedTime component to prevent parent re-renders
- Add isExpanded prop to conditionally render markdown content
- Implement content debouncing during streaming (~100ms)
- Pass isExpanded from ChatRow to ReasoningBlock

This addresses the performance regression where the 1Hz timer was causing
excessive re-renders of the entire reasoning block component tree.

Fixes #7999
This commit is contained in:
Roo Code 2025-09-23 01:22:20 +00:00
parent 0e1b23d09c
commit bc724952a4
3 changed files with 68 additions and 22 deletions

View file

@ -1039,6 +1039,7 @@ export const ChatRowContent = ({
ts={message.ts}
isStreaming={isStreaming}
isLast={isLast}
isExpanded={isExpanded}
metadata={message.metadata as any}
/>
)

View file

@ -0,0 +1,41 @@
import React, { memo, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
interface ElapsedTimeProps {
isStreaming: boolean
isLast: boolean
}
/**
* Isolated timer component that updates independently from parent.
* This prevents the entire ReasoningBlock from re-rendering every second.
*/
export const ElapsedTime = memo(({ isStreaming, isLast }: ElapsedTimeProps) => {
const { t } = useTranslation()
const startTimeRef = useRef<number>(Date.now())
const [elapsed, setElapsed] = useState<number>(0)
useEffect(() => {
if (isLast && isStreaming) {
const tick = () => setElapsed(Date.now() - startTimeRef.current)
tick()
const id = setInterval(tick, 1000)
return () => clearInterval(id)
}
}, [isLast, isStreaming])
const seconds = Math.floor(elapsed / 1000)
const secondsLabel = t("chat:reasoning.seconds", { count: seconds })
if (elapsed === 0) {
return null
}
return (
<span className="text-sm text-vscode-descriptionForeground tabular-nums flex items-center gap-1">
{secondsLabel}
</span>
)
})
ElapsedTime.displayName = "ElapsedTime"

View file

@ -1,40 +1,46 @@
import React, { useEffect, useRef, useState } from "react"
import React, { memo, useState, useEffect } from "react"
import { useTranslation } from "react-i18next"
import MarkdownBlock from "../common/MarkdownBlock"
import { Lightbulb } from "lucide-react"
import { ElapsedTime } from "./ElapsedTime"
interface ReasoningBlockProps {
content: string
ts: number
isStreaming: boolean
isLast: boolean
isExpanded?: boolean
metadata?: any
}
/**
* Render reasoning with a heading and a simple timer.
* - Heading uses i18n key chat:reasoning.thinking
* - Timer runs while reasoning is active (no persistence)
* - Timer is isolated in ElapsedTime component to prevent parent re-renders
* - Content is debounced during streaming to reduce re-render frequency
*/
export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockProps) => {
export const ReasoningBlock = memo(({ content, isStreaming, isLast, isExpanded = false }: ReasoningBlockProps) => {
const { t } = useTranslation()
const startTimeRef = useRef<number>(Date.now())
const [elapsed, setElapsed] = useState<number>(0)
// Debounce content updates during streaming
const [debouncedContent, setDebouncedContent] = useState(content)
// Simple timer that runs while streaming
useEffect(() => {
if (isLast && isStreaming) {
const tick = () => setElapsed(Date.now() - startTimeRef.current)
tick()
const id = setInterval(tick, 1000)
return () => clearInterval(id)
if (isStreaming) {
// Debounce content updates to ~10 updates per second max
const timer = setTimeout(() => {
setDebouncedContent(content)
}, 100)
return () => clearTimeout(timer)
} else {
// Immediately update when streaming ends
setDebouncedContent(content)
}
}, [isLast, isStreaming])
}, [content, isStreaming])
const seconds = Math.floor(elapsed / 1000)
const secondsLabel = t("chat:reasoning.seconds", { count: seconds })
// Only render markdown if expanded and content exists
const shouldRenderMarkdown = isExpanded && (debouncedContent?.trim()?.length ?? 0) > 0
return (
<div>
@ -43,17 +49,15 @@ export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockP
<Lightbulb className="w-4" />
<span className="font-bold text-vscode-foreground">{t("chat:reasoning.thinking")}</span>
</div>
{elapsed > 0 && (
<span className="text-sm text-vscode-descriptionForeground tabular-nums flex items-center gap-1">
{secondsLabel}
</span>
)}
<ElapsedTime isStreaming={isStreaming} isLast={isLast} />
</div>
{(content?.trim()?.length ?? 0) > 0 && (
{shouldRenderMarkdown && (
<div className="border-l border-vscode-descriptionForeground/20 ml-2 pl-4 pb-1 text-vscode-descriptionForeground">
<MarkdownBlock markdown={content} />
<MarkdownBlock markdown={debouncedContent} />
</div>
)}
</div>
)
}
})
ReasoningBlock.displayName = "ReasoningBlock"