Skip to content

Commit 635eb98

Browse files
committed
feat: add content debouncing to ReasoningBlock
- Implement debounced content updates during streaming (100ms debounce) - Combine with existing memoized ElapsedTime component for timer isolation - Prevents excessive re-renders during rapid content streaming - Immediate updates when streaming completes for final content - Addresses performance concerns raised in issue #7999 by @hannesrudolph
1 parent d03bf9a commit 635eb98

File tree

1 file changed

+36
-3
lines changed

1 file changed

+36
-3
lines changed

webview-ui/src/components/chat/ReasoningBlock.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React, { memo, useEffect, useRef, useState } from "react"
1+
import React, { memo, useEffect, useRef, useState, useMemo } from "react"
22
import { useTranslation } from "react-i18next"
3+
import debounce from "debounce"
34

45
import MarkdownBlock from "../common/MarkdownBlock"
56
import { Clock, Lightbulb } from "lucide-react"
@@ -56,13 +57,45 @@ ElapsedTime.displayName = "ElapsedTime"
5657
* - Heading uses i18n key chat:reasoning.thinking
5758
* - Timer runs while reasoning is active (no persistence)
5859
* - Timer is isolated in a memoized component to prevent full re-renders
60+
* - Content updates are debounced to prevent excessive re-renders during streaming
5961
*/
6062
export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockProps) => {
6163
const { t } = useTranslation()
6264
const startTimeRef = useRef<number>(Date.now())
65+
const [debouncedContent, setDebouncedContent] = useState<string>(content || "")
66+
67+
// Create a debounced function to update content
68+
// This limits content updates to a maximum of ~10 per second (100ms debounce)
69+
const updateDebouncedContent = useMemo(
70+
() =>
71+
debounce((newContent: string) => {
72+
setDebouncedContent(newContent)
73+
}, 100),
74+
[],
75+
)
76+
77+
// Update debounced content when content changes
78+
useEffect(() => {
79+
if (isStreaming) {
80+
// During streaming, use debounced updates
81+
updateDebouncedContent(content || "")
82+
} else {
83+
// When not streaming, update immediately for final content
84+
setDebouncedContent(content || "")
85+
// Cancel any pending debounced updates
86+
updateDebouncedContent.clear()
87+
}
88+
}, [content, isStreaming, updateDebouncedContent])
89+
90+
// Cleanup debounce on unmount
91+
useEffect(() => {
92+
return () => {
93+
updateDebouncedContent.clear()
94+
}
95+
}, [updateDebouncedContent])
6396

6497
// Only render markdown when there's actual content
65-
const hasContent = (content?.trim()?.length ?? 0) > 0
98+
const hasContent = (debouncedContent?.trim()?.length ?? 0) > 0
6699

67100
return (
68101
<div className="py-1">
@@ -75,7 +108,7 @@ export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockP
75108
</div>
76109
{hasContent && (
77110
<div className="px-3 italic text-vscode-descriptionForeground">
78-
<MarkdownBlock markdown={content} />
111+
<MarkdownBlock markdown={debouncedContent} />
79112
</div>
80113
)}
81114
</div>

0 commit comments

Comments
 (0)