React组件渲染时内存占用持续升高,是否存在内存泄漏?
我开发了一个React组件,通过WebSocket连接服务器并监听接收消息进行展示。当消息列表超过10条时,会移除第一条消息并添加新消息,确保消息数组长度始终不超过10,且单条消息数据量极小。
组件代码如下:
import React, { useEffect, useState, useRef } from "react"; const ws = new WebSocket("ws://localhost:8000?uid=React"); export default function Discord () { const [messages, setMessages] = useState([]); ws.addEventListener("message", data => { if (messages.length >= 10) { setMessages(arr => arr.slice(1,arr.length).concat(JSON.parse(data.data))); } else setMessages([...messages, JSON.parse(data.data)]); }); return( <div class="bg-gray-900 rounded-lg h-full w-full"> <div class="relative overflow-y-scroll scroll-smooth xl:h-4/5 h-5/6 top-0 w-full"> <div class="absolute"> <MessageList messages={messages}/> </div> </div> </div> ); }; function MessageList ({ messages }) { const messagesEndRef = useRef(null) useEffect(() => { messagesEndRef.current.scrollIntoView() }); return ( <ul class=""> {messages.map(message => <DiscordMessage messageData={message}/>)} <div ref={messagesEndRef}/> </ul> ) }
我已确认是该组件导致内存问题:移除组件后内存稳定在约600MB,启用后内存飙升至6GB,甚至导致Firefox崩溃。请问该内存问题是否源于组件渲染方式错误?
问题根源分析
你的内存问题主要不是渲染方式错误,而是WebSocket事件监听的注册逻辑有严重问题,同时搭配了不合理的useEffect使用,双重叠加导致内存泄漏:
WebSocket监听重复注册
const ws = new WebSocket("...")写在组件外部,每次组件重新渲染时,同一个WebSocket实例会被重复绑定N多个消息监听回调。每来一条新消息,所有绑定的回调都会触发并更新状态,导致状态更新次数指数级增长,内存里堆积大量闭包和状态快照,最终内存爆炸。useEffect缺少依赖项
MessageList里的useEffect(() => { ... })没有设置依赖数组,每次组件渲染(包括messages更新、父组件触发的重渲染)都会执行scrollIntoView,频繁的DOM操作加重内存负担,也造成不必要的性能消耗。组件卸载时未清理资源
组件被卸载时,没有移除WebSocket的message监听,也没有关闭连接,导致WebSocket实例一直持有回调函数的引用,回调又持有组件的state和上下文,这些资源无法被垃圾回收,持续占用内存。
修复方案
按以下步骤修改代码,彻底解决内存问题:
1. 规范WebSocket实例的创建与监听管理
import React, { useEffect, useState, useRef } from "react"; export default function Discord () { const [messages, setMessages] = useState([]); const wsRef = useRef(null); // 用ref保存WebSocket实例,避免重复创建 useEffect(() => { // 创建WebSocket实例 const ws = new WebSocket("ws://localhost:8000?uid=React"); wsRef.current = ws; // 定义消息处理回调 const handleMessage = (data) => { const newMessage = JSON.parse(data.data); setMessages(prev => { return prev.length >= 10 ? prev.slice(1).concat(newMessage) : [...prev, newMessage]; }); }; // 绑定监听 ws.addEventListener("message", handleMessage); // 组件卸载时清理资源 return () => { ws.removeEventListener("message", handleMessage); ws.close(); // 关闭WebSocket连接 }; }, []); // 空依赖数组,仅在组件挂载时执行一次 return( <div className="bg-gray-900 rounded-lg h-full w-full"> {/* React中用className替代class */} <div className="relative overflow-y-scroll scroll-smooth xl:h-4/5 h-5/6 top-0 w-full"> <div className="absolute"> <MessageList messages={messages}/> </div> </div> </div> ); };
2. 修复MessageList的useEffect依赖项
function MessageList ({ messages }) { const messagesEndRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // 仅当messages变化时执行滚动操作 return ( <ul className=""> {messages.map((message, index) => ( // 添加唯一key,优化React渲染逻辑 <DiscordMessage key={index} // 若message有唯一id字段,优先使用id作为key messageData={message} /> ))} <div ref={messagesEndRef}/> </ul> ) }
额外注意点
- React元素的类名必须用
className而非class,这是React的语法规范。 - 列表渲染时务必添加唯一
key,帮助React正确识别元素变化,减少不必要的DOM操作,避免潜在内存问题。
内容的提问来源于stack exchange,提问作者yaki06

