refactor(useSmoothStream): optimize chunk handling and state management (#8514)

* refactor(useSmoothStream): optimize chunk handling and state management

- Replaced state management with refs for chunk queue to improve performance and reduce unnecessary re-renders.
- Introduced Intl.Segmenter for better character segmentation based on language support.
- Updated rendering logic to ensure final text is displayed correctly when the stream ends.
- Cleaned up unused effects and comments for improved code clarity.

* refactor(useSmoothStream): move segmenter initialization outside of addChunk function

- Moved the initialization of Intl.Segmenter to the top of the file for better performance and to avoid redundant instantiation within the addChunk callback.
- This change enhances the efficiency of chunk processing in the useSmoothStream hook.
This commit is contained in:
MyPrototypeWhat 2025-07-25 19:16:22 +08:00 committed by GitHub
parent 6cc29c5005
commit 84157f7bd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef } from 'react'
interface UseSmoothStreamOptions {
onUpdate: (text: string) => void
@ -7,15 +7,18 @@ interface UseSmoothStreamOptions {
initialText?: string
}
const languages = ['en-US', 'es-ES', 'zh-CN', 'zh-TW', 'ja-JP', 'ru-RU', 'el-GR', 'fr-FR', 'pt-PT']
const segmenter = new Intl.Segmenter(languages)
export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialText = '' }: UseSmoothStreamOptions) => {
const [chunkQueue, setChunkQueue] = useState<string[]>([])
const chunkQueueRef = useRef<string[]>([])
const animationFrameRef = useRef<number | null>(null)
const displayedTextRef = useRef<string>(initialText)
const lastUpdateTimeRef = useRef<number>(0)
const addChunk = useCallback((chunk: string) => {
const chars = Array.from(chunk)
setChunkQueue((prev) => [...prev, ...(chars || [])])
const chars = Array.from(segmenter.segment(chunk)).map((s) => s.segment)
chunkQueueRef.current = [...chunkQueueRef.current, ...(chars || [])]
}, [])
const reset = useCallback(
@ -23,7 +26,7 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
setChunkQueue([])
chunkQueueRef.current = []
displayedTextRef.current = newText
onUpdate(newText)
},
@ -32,12 +35,16 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe
const renderLoop = useCallback(
(currentTime: number) => {
// 1. 如果队列为空,等待下一帧
if (chunkQueue.length === 0) {
// 如果流还没结束但队列空了,就等待下一帧
if (!streamDone) {
animationFrameRef.current = requestAnimationFrame(renderLoop)
// 1. 如果队列为空
if (chunkQueueRef.current.length === 0) {
// 如果流已结束,确保显示最终状态并停止循环
if (streamDone) {
const finalText = displayedTextRef.current
onUpdate(finalText)
return
}
// 如果流还没结束但队列空了,等待下一帧
animationFrameRef.current = requestAnimationFrame(renderLoop)
return
}
@ -48,25 +55,29 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe
}
lastUpdateTimeRef.current = currentTime
setChunkQueue((prevQueue) => {
// 3. 动态计算本次渲染的字符数
// 如果队列积压严重,就一次性渲染更多字符来"追赶"
const charsToRenderCount = Math.max(1, Math.floor(prevQueue.length / 5)) // 每次至少渲染1个最多渲染队列的1/5
// 3. 动态计算本次渲染的字符数
let charsToRenderCount = Math.max(1, Math.floor(chunkQueueRef.current.length / 5))
const charsToRender = prevQueue.slice(0, charsToRenderCount)
displayedTextRef.current += charsToRender.join('')
// 如果流已结束,一次性渲染所有剩余字符
if (streamDone) {
charsToRenderCount = chunkQueueRef.current.length
}
// 4. 立即更新UI
onUpdate(displayedTextRef.current)
const charsToRender = chunkQueueRef.current.slice(0, charsToRenderCount)
displayedTextRef.current += charsToRender.join('')
// 返回新的队列
return prevQueue.slice(charsToRenderCount)
})
// 4. 立即更新UI
onUpdate(displayedTextRef.current)
// 5. 请求下一帧动画
animationFrameRef.current = requestAnimationFrame(renderLoop)
// 5. 更新队列
chunkQueueRef.current = chunkQueueRef.current.slice(charsToRenderCount)
// 6. 如果还有内容需要渲染,继续下一帧
if (chunkQueueRef.current.length > 0) {
animationFrameRef.current = requestAnimationFrame(renderLoop)
}
},
[chunkQueue, streamDone, onUpdate, minDelay]
[streamDone, onUpdate, minDelay]
)
useEffect(() => {
@ -81,22 +92,5 @@ export const useSmoothStream = ({ onUpdate, streamDone, minDelay = 10, initialTe
}
}, [renderLoop])
// 当外部流结束,且队列即将变空时,进行最后一次"瞬移"渲染
useEffect(() => {
if (streamDone && chunkQueue.length > 0) {
const remainingText = chunkQueue.join('')
const finalText = displayedTextRef.current + remainingText
// 取消正在进行的动画循环
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
// 直接更新到最终状态
onUpdate(finalText)
setChunkQueue([]) // 清空队列
}
}, [streamDone, chunkQueue, onUpdate])
return { addChunk, reset }
}