5月28日 03:20

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 里给 windowdocument 或 DOM 节点添加了 resizescrollkeydown 等监听,但 cleanup 里没有 removeEventListener。监听回调持有组件作用域的引用,组件实例无法被 GC 回收。

javascript
useEffect(() => { 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 实例)同理,组件卸载时必须调用对应的销毁方法。

javascript
useEffect(() => { const ws = new WebSocket('wss://example.com'); ws.onmessage = (e) => setMessage(JSON.parse(e.data)); return () => ws.close(); }, []);

修复思路

核心原则:useEffect 的 setup 和 cleanup 必须对称——setup 里获取了什么资源,cleanup 里就要释放什么。具体做法:

  1. 异步请求用 AbortController 取消
  2. 定时器用 clearTimeout/clearInterval 清除
  3. DOM 事件用 removeEventListener 移除
  4. 订阅类 API 调用 close()/disconnect()/unsubscribe()
  5. 大对象引用用 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 的正确性要求更高。

标签:React前端