React 项目中常见的内存泄漏场景有哪些?
核心场景
React 组件卸载后,与之关联的副作用如果没有同步清除,就会产生内存泄漏。实际项目中主要分四类:
异步操作更新已卸载组件的状态
在组件内发起 fetch 请求,组件卸载了请求才回来,此时 setState 会触发 React 的 warning(React 18 下已移除该 warning,但逻辑上的泄漏仍然存在)。setTimeout/setInterval 同理——组件卸载了定时器还在跑,回调里访问了过期的 state 或调用 setState。
javascript// 泄漏写法 useEffect(() => { fetch('/api/data').then(res => res.json()).then(setData); }, []); // 修复:用 AbortController 取消请求 useEffect(() => { const controller = new AbortController(); fetch('/api/data', { signal: controller.signal }) .then(res => res.json()) .then(setData) .catch(err => { if (err.name !== 'AbortError') throw err; }); return () => controller.abort(); }, []);
事件监听未移除
在 useEffect 里给 window、document 或 DOM 节点添加了 resize、scroll、keydown 等监听,但 cleanup 里没有 removeEventListener。监听回调持有组件作用域的引用,组件实例无法被 GC 回收。
javascriptuseEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []);
闭包持有大对象引用
useCallback/useMemo/useRef 的闭包里引用了不再需要的大对象(比如接口返回的完整列表、Blob 数据等),只要闭包还在,这些对象就无法被回收。常见于列表页缓存整个数据集却只展示部分。
javascript// 泄漏:闭包引用了全量数据 const filteredList = useMemo(() => largeList.filter(fn), [largeList]); // 优化:只保留需要的字段,尽早释放原引用 const filteredList = useMemo( () => largeList.map(item => ({ id: item.id, name: item.name })).filter(fn), [largeList] );
未清理的订阅与第三方实例
WebSocket、EventSource、IntersectionObserver、ResizeObserver、MutationObserver 这些 API 都需要手动 close()/disconnect()。第三方库(如 Redux 的 subscribe、RxJS 的 Observable.subscribe、ECharts 实例)同理,组件卸载时必须调用对应的销毁方法。
javascriptuseEffect(() => { const ws = new WebSocket('wss://example.com'); ws.onmessage = (e) => setMessage(JSON.parse(e.data)); return () => ws.close(); }, []);
修复思路
核心原则:useEffect 的 setup 和 cleanup 必须对称——setup 里获取了什么资源,cleanup 里就要释放什么。具体做法:
- 异步请求用
AbortController取消 - 定时器用
clearTimeout/clearInterval清除 - DOM 事件用
removeEventListener移除 - 订阅类 API 调用
close()/disconnect()/unsubscribe() - 大对象引用用
useRef配合手动置null释放
追问
useEffect 的 cleanup 什么时候执行?
组件卸载时,或者下一次 effect 执行前(依赖项变化时先跑旧 cleanup 再跑新 effect)。React 18 StrictMode 下开发模式会额外执行一次 setup+cleanup 来暴露遗漏的清理逻辑。
怎么排查 React 内存泄漏?
Chrome DevTools → Memory 面板 → 取 Heap Snapshot。操作组件(挂载→卸载→挂载→卸载),对比两次 snapshot。如果 Detached DOM 节点或组件实例数量持续上升,说明有泄漏。也可以用 React DevTools Profiler 观察 React 组件树是否出现已卸载组件仍存在的现象。
React 18 对内存泄漏有什么影响?
React 18 移除了 "Can't perform a React state update on an unmounted component" 的 warning,但泄漏本身并未消失——未清理的闭包引用和事件监听仍然存在。同时 StrictMode 在开发模式下双重挂载组件,更容易暴露 cleanup 遗漏的问题。此外并发特性(Suspense、useTransition)引入了组件挂载-卸载-重新挂载的新生命周期,对 cleanup 的正确性要求更高。