Appearance
内存泄漏识别与修复 - React应用内存管理指南
1. 什么是内存泄漏
1.1 内存泄漏定义
typescript
const memoryLeakDefinition = {
定义: '应用程序不再需要的内存未被释放,导致可用内存逐渐减少',
表现: [
'页面响应变慢',
'浏览器标签页崩溃',
'内存使用持续增长',
'垃圾回收频繁触发'
],
常见原因: [
'未清理的事件监听器',
'未清理的定时器',
'未关闭的连接',
'DOM引用未释放',
'闭包持有大对象',
'全局变量累积'
]
};1.2 JavaScript内存管理基础
javascript
// 垃圾回收示例
function createUser() {
let user = { // 分配内存
name: 'John',
age: 30
};
return user;
}
let userData = createUser(); // user对象被引用
userData = null; // 取消引用,对象可被垃圾回收
// 闭包保持引用
function createClosure() {
let largeArray = new Array(1000000).fill('data'); // 分配大量内存
return function() {
// 闭包持有largeArray的引用
console.log(largeArray.length);
};
}
const closure = createClosure(); // largeArray不会被回收
// closure = null; // 释放闭包后,largeArray才能被回收1.3 React中的内存泄漏
jsx
// ❌ 典型内存泄漏: 组件卸载后仍然setState
function BadComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
// 如果组件已卸载,这里会内存泄漏并警告
setData(data);
});
}, []);
return <div>{data}</div>;
}
// ✅ 正确: 检查组件是否已卸载
function GoodComponent() {
const [data, setData] = useState(null);
useEffect(() => {
let cancelled = false;
fetch('/api/data')
.then(res => res.json())
.then(data => {
if (!cancelled) {
setData(data);
}
});
return () => {
cancelled = true;
};
}, []);
return <div>{data}</div>;
}2. 常见内存泄漏场景
2.1 未清理的事件监听器
jsx
// ❌ 内存泄漏: 忘记移除事件监听器
function BadEventListener() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = () => {
setCount(c => c + 1);
};
document.addEventListener('click', handleClick);
// 忘记return清理函数!
// 组件卸载后,监听器仍然存在,持有组件引用
}, []);
return <div>Clicks: {count}</div>;
}
// ✅ 正确: 清理事件监听器
function GoodEventListener() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = () => {
setCount(c => c + 1);
};
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
}, []);
return <div>Clicks: {count}</div>;
}
// ✅ 自定义Hook封装
function useClickCount() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = () => setCount(c => c + 1);
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []);
return count;
}
function ComponentUsingHook() {
const clickCount = useClickCount();
return <div>Clicks: {clickCount}</div>;
}2.2 未清理的定时器
jsx
// ❌ 内存泄漏: 定时器未清理
function BadTimer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// 忘记清理!
// 组件卸载后,定时器仍在运行
}, []);
return <div>{seconds}s</div>;
}
// ✅ 正确: 清理定时器
function GoodTimer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <div>{seconds}s</div>;
}
// ❌ 多个定时器累积
function BadMultipleTimers({ interval }) {
const [count, setCount] = useState(0);
useEffect(() => {
// 每次interval变化都创建新定时器
const timer = setInterval(() => {
setCount(c => c + 1);
}, interval);
// 忘记清理旧定时器!
}, [interval]);
return <div>{count}</div>;
}
// ✅ 正确: 清理旧定时器
function GoodMultipleTimers({ interval }) {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, interval);
return () => {
clearInterval(timer);
};
}, [interval]);
return <div>{count}</div>;
}2.3 未关闭的连接
jsx
// ❌ WebSocket未关闭
function BadWebSocket({ url }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
// 忘记关闭WebSocket!
}, [url]);
return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}
// ✅ 正确: 关闭WebSocket
function GoodWebSocket({ url }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => {
ws.close();
};
}, [url]);
return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}
// ❌ EventSource未关闭
function BadSSE() {
const [updates, setUpdates] = useState([]);
useEffect(() => {
const eventSource = new EventSource('/api/updates');
eventSource.onmessage = (event) => {
setUpdates(prev => [...prev, event.data]);
};
// 忘记关闭!
}, []);
return <div>{updates.length} updates</div>;
}
// ✅ 正确: 关闭EventSource
function GoodSSE() {
const [updates, setUpdates] = useState([]);
useEffect(() => {
const eventSource = new EventSource('/api/updates');
eventSource.onmessage = (event) => {
setUpdates(prev => [...prev, event.data]);
};
return () => {
eventSource.close();
};
}, []);
return <div>{updates.length} updates</div>;
}2.4 DOM引用泄漏
jsx
// ❌ 持有DOM引用
function BadDOMRef() {
const elementsRef = useRef([]);
useEffect(() => {
// 收集所有按钮元素
const buttons = document.querySelectorAll('button');
elementsRef.current = Array.from(buttons);
// 组件卸载后,仍持有DOM引用
}, []);
const logElements = () => {
console.log(elementsRef.current);
};
return <button onClick={logElements}>Log Elements</button>;
}
// ✅ 正确: 清理DOM引用
function GoodDOMRef() {
const elementsRef = useRef([]);
useEffect(() => {
const buttons = document.querySelectorAll('button');
elementsRef.current = Array.from(buttons);
return () => {
elementsRef.current = []; // 清理引用
};
}, []);
const logElements = () => {
console.log(elementsRef.current);
};
return <button onClick={logElements}>Log Elements</button>;
}
// ❌ IntersectionObserver未断开
function BadIntersectionObserver() {
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
console.log(entries);
});
if (ref.current) {
observer.observe(ref.current);
}
// 忘记断开观察!
}, []);
return <div ref={ref}>Observed Element</div>;
}
// ✅ 正确: 断开观察
function GoodIntersectionObserver() {
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
console.log(entries);
});
const element = ref.current;
if (element) {
observer.observe(element);
}
return () => {
if (element) {
observer.unobserve(element);
}
observer.disconnect();
};
}, []);
return <div ref={ref}>Observed Element</div>;
}2.5 闭包持有大对象
jsx
// ❌ 闭包持有大对象
function BadClosure() {
const [count, setCount] = useState(0);
// 大对象
const largeData = useMemo(() => {
return new Array(1000000).fill({ value: 'data' });
}, []);
useEffect(() => {
const timer = setInterval(() => {
// 闭包捕获了largeData
console.log('Count:', count, 'Data size:', largeData.length);
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count, largeData]); // largeData在依赖中
return <div>{count}</div>;
}
// ✅ 解决方案1: 避免在闭包中使用大对象
function ClosureSolution1() {
const [count, setCount] = useState(0);
const largeData = useMemo(() => {
return new Array(1000000).fill({ value: 'data' });
}, []);
// 保存大小而不是整个对象
const dataSize = largeData.length;
useEffect(() => {
const timer = setInterval(() => {
console.log('Count:', count, 'Data size:', dataSize);
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count, dataSize]); // 只依赖数字
return <div>{count}</div>;
}
// ✅ 解决方案2: 使用useRef
function ClosureSolution2() {
const [count, setCount] = useState(0);
const largeDataRef = useRef(
new Array(1000000).fill({ value: 'data' })
);
useEffect(() => {
const timer = setInterval(() => {
// ref不需要在依赖中
console.log('Count:', count, 'Data size:', largeDataRef.current.length);
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
// 组件卸载时清理
useEffect(() => {
return () => {
largeDataRef.current = null;
};
}, []);
return <div>{count}</div>;
}2.6 第三方库集成
jsx
// ❌ Chart.js未销毁
function BadChart({ data }) {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null);
useEffect(() => {
if (chartRef.current) {
chartInstanceRef.current = new Chart(chartRef.current, {
type: 'line',
data: data
});
}
// 忘记销毁图表实例!
}, [data]);
return <canvas ref={chartRef} />;
}
// ✅ 正确: 销毁图表实例
function GoodChart({ data }) {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null);
useEffect(() => {
if (chartRef.current) {
// 先销毁旧实例
if (chartInstanceRef.current) {
chartInstanceRef.current.destroy();
}
chartInstanceRef.current = new Chart(chartRef.current, {
type: 'line',
data: data
});
}
return () => {
if (chartInstanceRef.current) {
chartInstanceRef.current.destroy();
chartInstanceRef.current = null;
}
};
}, [data]);
return <canvas ref={chartRef} />;
}
// ❌ React Query未清理缓存
function BadReactQuery() {
const { data } = useQuery({
queryKey: ['huge-dataset'],
queryFn: fetchHugeDataset,
cacheTime: Infinity // 永久缓存!
});
return <div>{data?.length} items</div>;
}
// ✅ 正确: 设置合理的缓存时间
function GoodReactQuery() {
const { data } = useQuery({
queryKey: ['huge-dataset'],
queryFn: fetchHugeDataset,
cacheTime: 5 * 60 * 1000, // 5分钟后清理
staleTime: 1 * 60 * 1000 // 1分钟后标记为过期
});
return <div>{data?.length} items</div>;
}3. 内存泄漏检测
3.1 Chrome DevTools Memory Profiler
typescript
const memoryProfilerGuide = {
步骤: [
'1. 打开Chrome DevTools',
'2. 切换到Memory选项卡',
'3. 选择"Heap snapshot"',
'4. 点击"Take snapshot"',
'5. 执行操作(如打开/关闭组件)',
'6. 再次Take snapshot',
'7. 选择"Comparison"视图比较两个快照',
'8. 查看增长的对象和引用链'
],
关键指标: [
'Shallow Size: 对象本身的大小',
'Retained Size: 对象及其引用的总大小',
'Distance: 到GC根的距离',
'Detached DOM: 已分离但未释放的DOM节点'
],
常见泄漏模式: [
'Detached DOM nodes',
'过多的闭包',
'大的数组/对象持续增长',
'未清理的事件监听器'
]
};3.2 Performance Monitor
typescript
const performanceMonitorGuide = {
使用: [
'1. Chrome DevTools -> More tools -> Performance monitor',
'2. 观察"JS heap size"曲线',
'3. 正常应用: 锯齿状(GC后下降)',
'4. 内存泄漏: 持续上升'
],
指标: [
'JS heap size: JavaScript堆内存大小',
'DOM Nodes: DOM节点数量',
'JS event listeners: 事件监听器数量',
'CPU usage: CPU使用率'
]
};3.3 自定义内存监控
jsx
// 创建内存监控Hook
function useMemoryMonitor(componentName) {
useEffect(() => {
if (performance.memory) {
const logMemory = () => {
const { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit } = performance.memory;
console.log(`[Memory] ${componentName}:`, {
used: `${(usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
total: `${(totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
limit: `${(jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB`,
usage: `${((usedJSHeapSize / jsHeapSizeLimit) * 100).toFixed(2)}%`
});
};
logMemory();
const interval = setInterval(logMemory, 5000);
return () => {
clearInterval(interval);
console.log(`[Memory] ${componentName} unmounted`);
};
}
}, [componentName]);
}
// 使用
function MonitoredComponent() {
useMemoryMonitor('MonitoredComponent');
const [items, setItems] = useState([]);
const addItems = () => {
setItems(prev => [...prev, ...new Array(10000).fill('item')]);
};
return (
<div>
<button onClick={addItems}>Add 10k Items</button>
<p>{items.length} items</p>
</div>
);
}3.4 监听器泄漏检测
jsx
// 检测事件监听器泄漏
function useEventListenerDetector() {
useEffect(() => {
const originalAddEventListener = EventTarget.prototype.addEventListener;
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
const listeners = new Map();
let listenerId = 0;
EventTarget.prototype.addEventListener = function(type, listener, options) {
const id = listenerId++;
listeners.set(id, {
target: this,
type,
listener,
options,
stack: new Error().stack
});
console.log(`[Listener Added] #${id}:`, type, 'Total:', listeners.size);
return originalAddEventListener.call(this, type, listener, options);
};
EventTarget.prototype.removeEventListener = function(type, listener, options) {
// 查找并移除
for (const [id, item] of listeners.entries()) {
if (item.target === this && item.type === type && item.listener === listener) {
listeners.delete(id);
console.log(`[Listener Removed] #${id}:`, type, 'Total:', listeners.size);
break;
}
}
return originalRemoveEventListener.call(this, type, listener, options);
};
// 定期报告泄漏
const reportInterval = setInterval(() => {
if (listeners.size > 0) {
console.warn(`[Listener Leak] ${listeners.size} listeners not removed:`,
Array.from(listeners.values()).map(l => ({
type: l.type,
target: l.target.constructor.name
}))
);
}
}, 10000);
return () => {
clearInterval(reportInterval);
EventTarget.prototype.addEventListener = originalAddEventListener;
EventTarget.prototype.removeEventListener = originalRemoveEventListener;
};
}, []);
}
// 在开发环境使用
function App() {
if (process.env.NODE_ENV === 'development') {
useEventListenerDetector();
}
return <YourApp />;
}4. 修复策略
4.1 cleanup函数模式
jsx
// 标准cleanup模式
function useCleanupPattern(subscribe) {
useEffect(() => {
// 1. 设置资源
const resource = subscribe();
// 2. 返回清理函数
return () => {
// 3. 清理资源
resource.unsubscribe();
};
}, [subscribe]);
}
// 多个cleanup
function useMultipleCleanup() {
useEffect(() => {
// 设置多个资源
const timer = setInterval(() => {}, 1000);
const listener = () => {};
window.addEventListener('resize', listener);
// 清理所有资源
return () => {
clearInterval(timer);
window.removeEventListener('resize', listener);
};
}, []);
}
// 条件cleanup
function useConditionalCleanup(shouldSubscribe) {
useEffect(() => {
if (!shouldSubscribe) return;
const subscription = subscribe();
return () => {
subscription.unsubscribe();
};
}, [shouldSubscribe]);
}4.2 AbortController模式
jsx
// 使用AbortController取消请求
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
setLoading(true);
setError(null);
fetch(url, {
signal: abortController.signal
})
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => {
abortController.abort();
};
}, [url]);
return { data, loading, error };
}
// 多个请求共享AbortController
function useMultipleFetch(urls) {
const [results, setResults] = useState([]);
useEffect(() => {
const abortController = new AbortController();
Promise.all(
urls.map(url =>
fetch(url, { signal: abortController.signal })
.then(res => res.json())
)
)
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => {
abortController.abort();
};
}, [urls]);
return results;
}4.3 WeakMap/WeakSet模式
jsx
// 使用WeakMap避免内存泄漏
function useWeakMapCache() {
const cache = useRef(new WeakMap());
const cacheValue = useCallback((key, value) => {
// WeakMap的key必须是对象
// 当key对象被回收时,对应的value也会被回收
cache.current.set(key, value);
}, []);
const getValue = useCallback((key) => {
return cache.current.get(key);
}, []);
return { cacheValue, getValue };
}
// 使用WeakSet跟踪对象
function useWeakSetTracker() {
const tracked = useRef(new WeakSet());
const track = useCallback((obj) => {
tracked.current.add(obj);
}, []);
const isTracked = useCallback((obj) => {
return tracked.current.has(obj);
}, []);
return { track, isTracked };
}4.4 资源池模式
jsx
// 创建对象池避免频繁创建
class ObjectPool {
constructor(createFn, resetFn, size = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.active = new Set();
// 预创建对象
for (let i = 0; i < size; i++) {
this.pool.push(this.createFn());
}
}
acquire() {
let obj = this.pool.pop();
if (!obj) {
obj = this.createFn();
}
this.active.add(obj);
return obj;
}
release(obj) {
if (this.active.has(obj)) {
this.active.delete(obj);
this.resetFn(obj);
this.pool.push(obj);
}
}
clear() {
this.pool = [];
this.active.clear();
}
}
// 使用对象池
function useObjectPool() {
const poolRef = useRef(
new ObjectPool(
() => ({ data: null, timestamp: null }),
(obj) => { obj.data = null; obj.timestamp = null; },
20
)
);
useEffect(() => {
return () => {
poolRef.current.clear();
};
}, []);
const acquire = useCallback(() => {
return poolRef.current.acquire();
}, []);
const release = useCallback((obj) => {
poolRef.current.release(obj);
}, []);
return { acquire, release };
}5. 最佳实践
5.1 组件生命周期管理
jsx
// 统一的资源管理Hook
function useResourceManager() {
const resourcesRef = useRef([]);
const registerResource = useCallback((cleanup) => {
resourcesRef.current.push(cleanup);
// 返回取消注册函数
return () => {
const index = resourcesRef.current.indexOf(cleanup);
if (index > -1) {
resourcesRef.current.splice(index, 1);
}
};
}, []);
useEffect(() => {
return () => {
// 组件卸载时清理所有资源
resourcesRef.current.forEach(cleanup => cleanup());
resourcesRef.current = [];
};
}, []);
return registerResource;
}
// 使用
function Component() {
const registerResource = useResourceManager();
useEffect(() => {
const timer = setInterval(() => {}, 1000);
// 注册清理函数
registerResource(() => clearInterval(timer));
}, [registerResource]);
useEffect(() => {
const listener = () => {};
window.addEventListener('resize', listener);
registerResource(() => {
window.removeEventListener('resize', listener);
});
}, [registerResource]);
return <div>Component</div>;
}5.2 防御式编程
jsx
// 安全的setState
function useSafeState(initialState) {
const [state, setState] = useState(initialState);
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
const safeSetState = useCallback((newState) => {
if (mountedRef.current) {
setState(newState);
}
}, []);
return [state, safeSetState];
}
// 安全的异步操作
function useSafeAsync(asyncFn, dependencies) {
const [data, setData] = useSafeState(null);
const [loading, setLoading] = useSafeState(true);
const [error, setError] = useSafeState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
asyncFn()
.then(result => {
if (!cancelled) {
setData(result);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, dependencies);
return { data, loading, error };
}5.3 性能监控
jsx
// 创建性能监控系统
function usePerformanceMonitor(componentName) {
const metricsRef = useRef({
renderCount: 0,
lastRenderTime: Date.now(),
mountTime: Date.now(),
memorySnapshots: []
});
// 记录渲染
metricsRef.current.renderCount++;
metricsRef.current.lastRenderTime = Date.now();
// 记录内存
useEffect(() => {
if (performance.memory) {
metricsRef.current.memorySnapshots.push({
time: Date.now(),
used: performance.memory.usedJSHeapSize
});
// 只保留最近50个快照
if (metricsRef.current.memorySnapshots.length > 50) {
metricsRef.current.memorySnapshots.shift();
}
}
});
// 卸载时报告
useEffect(() => {
return () => {
const lifetime = Date.now() - metricsRef.current.mountTime;
const snapshots = metricsRef.current.memorySnapshots;
if (snapshots.length > 1) {
const first = snapshots[0].used;
const last = snapshots[snapshots.length - 1].used;
const growth = last - first;
const growthRate = (growth / lifetime) * 1000; // bytes/second
console.log(`[Performance] ${componentName}:`, {
lifetime: `${(lifetime / 1000).toFixed(2)}s`,
renders: metricsRef.current.renderCount,
memoryGrowth: `${(growth / 1024 / 1024).toFixed(2)} MB`,
growthRate: `${(growthRate / 1024).toFixed(2)} KB/s`
});
// 警告内存泄漏
if (growthRate > 100 * 1024) { // > 100KB/s
console.warn(`[Memory Leak Warning] ${componentName} might have a memory leak`);
}
}
};
}, [componentName]);
return metricsRef.current;
}6. 测试内存泄漏
6.1 自动化测试
jsx
// Jest测试内存泄漏
import { render, unmountComponentAtNode } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
describe('Memory Leak Tests', () => {
let container = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
test('component cleans up event listeners', () => {
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
act(() => {
render(<ComponentWithListener />, container);
});
expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
act(() => {
unmountComponentAtNode(container);
});
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1);
addEventListenerSpy.mockRestore();
removeEventListenerSpy.mockRestore();
});
test('component cleans up timers', () => {
jest.useFakeTimers();
act(() => {
render(<ComponentWithTimer />, container);
});
const timerCount = jest.getTimerCount();
expect(timerCount).toBeGreaterThan(0);
act(() => {
unmountComponentAtNode(container);
});
expect(jest.getTimerCount()).toBe(0);
jest.useRealTimers();
});
});6.2 Cypress E2E测试
javascript
// cypress/integration/memory-leak.spec.js
describe('Memory Leak Detection', () => {
it('should not leak memory on repeated mount/unmount', () => {
cy.visit('/test-page');
// 记录初始内存
cy.window().then((win) => {
const initialMemory = win.performance.memory.usedJSHeapSize;
// 重复mount/unmount 10次
for (let i = 0; i < 10; i++) {
cy.get('[data-testid="mount-component"]').click();
cy.wait(100);
cy.get('[data-testid="unmount-component"]').click();
cy.wait(100);
}
// 强制GC (需要Chrome启动参数: --js-flags="--expose-gc")
if (win.gc) {
win.gc();
}
cy.wait(1000);
// 检查内存增长
cy.window().then((win) => {
const finalMemory = win.performance.memory.usedJSHeapSize;
const growth = finalMemory - initialMemory;
const growthMB = growth / 1024 / 1024;
cy.log(`Memory growth: ${growthMB.toFixed(2)} MB`);
// 内存增长应小于10MB
expect(growthMB).to.be.lessThan(10);
});
});
});
});7. 总结
内存泄漏是React应用中需要重点关注的问题:
- 常见原因: 未清理的监听器/定时器/连接、DOM引用、闭包
- 检测方法: Chrome DevTools、Performance Monitor、自定义监控
- 修复策略: cleanup函数、AbortController、WeakMap、对象池
- 最佳实践: 资源管理、防御式编程、性能监控
- 测试: 自动化测试、E2E测试
掌握这些知识可以有效避免和修复内存泄漏,构建高性能的React应用。