perf: part of memory leak (#8619)

* fix: 修复多个组件中的内存泄漏问题

清理setTimeout和事件监听器以避免内存泄漏
优化useEffect中的异步操作清理逻辑

* fix: review comments
This commit is contained in:
Phantom 2025-07-29 17:41:56 +08:00 committed by GitHub
parent 27af64f2bd
commit b716a7446a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 67 additions and 16 deletions

View File

@ -16,12 +16,22 @@ const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
}, [])
useEffect(() => {
if (ref.current) {
ref.current.addEventListener('emoji-click', (event: any) => {
const refValue = ref.current
if (refValue) {
const handleEmojiClick = (event: any) => {
event.stopPropagation()
onEmojiClick(event.detail.unicode || event.detail.emoji.unicode)
})
}
// 添加事件监听器
refValue.addEventListener('emoji-click', handleEmojiClick)
// 清理事件监听器
return () => {
refValue.removeEventListener('emoji-click', handleEmojiClick)
}
}
return
}, [onEmojiClick])
// @ts-ignore next-line

View File

@ -64,9 +64,9 @@ const WebviewContainer = memo(
webviewRef.current.src = url
return () => {
webviewRef.current?.removeEventListener('dom-ready', handleDomReady)
webviewRef.current?.removeEventListener('did-finish-load', handleLoaded)
webviewRef.current?.removeEventListener('did-navigate-in-page', handleNavigate)
webviewRef.current?.removeEventListener('dom-ready', handleDomReady)
}
// because the appid and url are enough, no need to add onLoadedCallback
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -162,6 +162,9 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
const onRemoveModel = useCallback((model: Model) => removeModel(model), [removeModel])
useEffect(() => {
let timer: NodeJS.Timeout
let mounted = true
runAsyncFunction(async () => {
try {
setLoading(true)
@ -188,18 +191,32 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
} catch (error) {
logger.error('Failed to fetch models', error as Error)
} finally {
setTimeout(() => setLoading(false), 300)
if (mounted) {
timer = setTimeout(() => setLoading(false), 300)
}
}
})
return () => {
mounted = false
if (timer) {
clearTimeout(timer)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (open && searchInputRef.current) {
setTimeout(() => {
const timer = setTimeout(() => {
searchInputRef.current?.focus()
}, 350)
return () => {
clearTimeout(timer)
}
}
return
}, [open])
const ModalHeader = () => {

View File

@ -141,7 +141,10 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
}
useEffect(() => {
open && setTimeout(() => inputRef.current?.focus(), 0)
if (!open) return
const timer = setTimeout(() => inputRef.current?.focus(), 0)
return () => clearTimeout(timer)
}, [open])
return (

View File

@ -346,7 +346,8 @@ const PopupContainer: React.FC<Props> = ({ model, resolve, modelFilter }) => {
// 初始化焦点和滚动位置
useEffect(() => {
if (!open) return
setTimeout(() => inputRef.current?.focus(), 0)
const timer = setTimeout(() => inputRef.current?.focus(), 0)
return () => clearTimeout(timer)
}, [open])
const togglePin = useCallback(

View File

@ -76,7 +76,8 @@ const PopupContainer: React.FC<Props> = ({
}
useEffect(() => {
setTimeout(resizeTextArea, 0)
const timer = setTimeout(resizeTextArea, 0)
return () => clearTimeout(timer)
}, [])
const handleAfterOpenChange = (visible: boolean) => {

View File

@ -51,11 +51,15 @@ const Selector = <V extends string | number>({
const inputRef = useRef<any>(null)
useEffect(() => {
let timer: NodeJS.Timeout
if (open) {
setTimeout(() => {
timer = setTimeout(() => {
inputRef.current?.focus()
}, 1)
}
return () => {
clearTimeout(timer)
}
}, [open])
const selectedValues = useMemo(() => {

View File

@ -22,7 +22,8 @@ export const TopNavbarOpenedMinappTabs: FC = () => {
const [keepAliveMinapps, setKeepAliveMinapps] = useState(openedKeepAliveMinapps)
useEffect(() => {
setTimeout(() => setKeepAliveMinapps(openedKeepAliveMinapps), 300)
const timer = setTimeout(() => setKeepAliveMinapps(openedKeepAliveMinapps), 300)
return () => clearTimeout(timer)
}, [openedKeepAliveMinapps])
const handleOnClick = (app) => {

View File

@ -709,7 +709,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}, [assistant, topic])
useEffect(() => {
setTimeout(() => resizeTextArea(), 0)
const timerId = requestAnimationFrame(() => resizeTextArea())
return () => cancelAnimationFrame(timerId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

View File

@ -31,6 +31,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
const updateSelectedWebSearchProvider = useCallback(
(providerId?: WebSearchProvider['id']) => {
// TODO: updateAssistant有性能问题会导致关闭快捷面板卡顿
// NOTE: 也许可以用startTransition优化卡顿问题
setTimeout(() => {
const currentWebSearchProviderId = assistant.webSearchProviderId
const newWebSearchProviderId = currentWebSearchProviderId === providerId ? undefined : providerId

View File

@ -446,12 +446,16 @@ const ChatFlowHistory: FC<ChatFlowHistoryProps> = ({ conversationId }) => {
useEffect(() => {
setLoading(true)
setTimeout(() => {
const timer = setTimeout(() => {
const { nodes: flowNodes, edges: flowEdges } = buildConversationFlowData()
setNodes([...flowNodes])
setEdges([...flowEdges])
setLoading(false)
}, 500)
return () => {
clearTimeout(timer)
}
}, [buildConversationFlowData, setNodes, setEdges])
return (

View File

@ -97,11 +97,13 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
}, [couldAddImageFile, couldAddTextFile])
useEffect(() => {
setTimeout(() => {
const timer = setTimeout(() => {
if (textareaRef.current) {
textareaRef.current.focus({ cursor: 'end' })
}
}, 0)
return () => clearTimeout(timer)
}, [])
// 仅在打开时执行一次

View File

@ -494,12 +494,18 @@ const CollapsedContent: FC<{ isExpanded: boolean; resultString: string }> = ({ i
const [styledResult, setStyledResult] = useState<string>('')
useEffect(() => {
if (!isExpanded) {
return
}
const highlight = async () => {
const result = await highlightCode(isExpanded ? resultString : '', 'json')
const result = await highlightCode(resultString, 'json')
setStyledResult(result)
}
setTimeout(highlight, 0)
const timer = setTimeout(highlight, 0)
return () => clearTimeout(timer)
}, [isExpanded, resultString, highlightCode])
if (!isExpanded) {