Skip to content

浏览器内存泄漏诊断

本文聚焦浏览器环境的内存问题,Node.js 内存管理见 nodejs/04-runtime-memory-gc.md

一、浏览器内存模型

┌────────────────────────────────────────┐
│              浏览器进程                 │
├────────────────────────────────────────┤
│  JS Heap  │  DOM Tree  │  渲染数据     │
│  (V8管理)  │  (C++对象)  │  (位图/层)    │
└────────────────────────────────────────┘

内存类型

类型来源监控方式
JS HeapJavaScript 对象performance.memory
DOM NodesDOM 元素节点计数
Event Listeners事件监听器getEventListeners()
GPU MemoryCanvas/WebGL开发者工具

二、常见泄漏场景

1. 事件监听未移除

javascript
// ❌ 组件卸载时未移除
class Component {
  mount() {
    window.addEventListener('resize', this.handleResize);
  }
  // 缺少 unmount 中的 removeEventListener
}

// ✅ 正确做法
class Component {
  mount() {
    window.addEventListener('resize', this.handleResize);
  }
  unmount() {
    window.removeEventListener('resize', this.handleResize);
  }
}

// ✅ React useEffect
useEffect(() => {
  const handler = () => {};
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

2. 定时器未清理

javascript
// ❌ 泄漏
function startPolling() {
  setInterval(() => {
    fetchData(); // 闭包引用大对象
  }, 1000);
}

// ✅ 保存引用并清理
let timerId;
function startPolling() {
  timerId = setInterval(() => fetchData(), 1000);
}
function stopPolling() {
  clearInterval(timerId);
}

3. Detached DOM 节点

javascript
// ❌ DOM 被移除,但 JS 仍持有引用
let element = document.getElementById('myElement');
element.remove();
// element 仍指向已移除的 DOM,无法回收

// ✅ 同步清理引用
element.remove();
element = null;

4. 闭包持有大对象

javascript
// ❌ 闭包捕获整个大数组
function createHandler() {
  const largeArray = new Array(1000000).fill('x');
  return function() {
    console.log(largeArray.length);
  };
}

// ✅ 只保留需要的数据
function createHandler() {
  const largeArray = new Array(1000000).fill('x');
  const length = largeArray.length;
  return function() {
    console.log(length);
  };
}

5. 全局变量

javascript
// ❌ 意外创建全局变量
function foo() {
  bar = 'leak'; // 没有 var/let/const
}

// ✅ 使用严格模式
'use strict';
function foo() {
  bar = 'leak'; // ReferenceError
}

三、DevTools 内存分析

Heap Snapshot (堆快照)

1. DevTools → Memory → Take Heap Snapshot
2. 执行可能泄漏的操作
3. 再次 Take Heap Snapshot
4. 选择 Comparison 视图对比

关注:
- Detached HTMLElement: 已移除但未回收的 DOM
- (closure): 闭包持有的对象
- 对象数量增长

Allocation Timeline

1. Memory → Record allocation timeline
2. 执行操作
3. 停止录制
4. 蓝色条 = 分配仍存活
5. 灰色条 = 已回收

Performance Monitor

1. More tools → Performance Monitor
2. 监控:
   - JS heap size
   - DOM Nodes
   - JS event listeners
   - Documents

四、代码级检测

javascript
// 内存使用 API
if (performance.memory) {
  console.log({
    usedJSHeapSize: performance.memory.usedJSHeapSize,
    totalJSHeapSize: performance.memory.totalJSHeapSize,
    jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
  });
}

// 弱引用检测对象回收
const ref = new WeakRef(largeObject);
// 稍后检查
if (ref.deref() === undefined) {
  console.log('Object was garbage collected');
}

// FinalizationRegistry 回收通知
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`${heldValue} was collected`);
});
registry.register(object, 'my-object');

五、React 内存泄漏

javascript
// ❌ 常见泄漏模式
function Component() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetchData().then(setData); // 组件卸载后仍可能执行
  }, []);
}

// ✅ 使用 AbortController
function Component() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    fetchData({ signal: controller.signal })
      .then(setData)
      .catch(e => {
        if (e.name !== 'AbortError') throw e;
      });
    
    return () => controller.abort();
  }, []);
}

// ✅ 使用标志位
function Component() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    let mounted = true;
    
    fetchData().then(result => {
      if (mounted) setData(result);
    });
    
    return () => { mounted = false; };
  }, []);
}

面试高频题

Q1: 如何判断是否有内存泄漏?

  1. 打开 Performance Monitor 监控 JS heap
  2. 反复执行某操作
  3. 手动触发 GC
  4. 如果内存持续增长不下降,可能有泄漏

Q2: 什么是 Detached DOM?

DOM 元素从树中移除,但 JavaScript 仍持有引用,导致无法回收。

Q3: WeakMap/WeakSet 如何帮助避免泄漏?

键是弱引用,当键对象没有其他引用时,会被 GC 回收,不会因为 Map 持有而保留。

前端面试知识库