前置知识: JavaScript

内存泄漏排查

00:00
3 min Advanced 2026/6/14

JavaScript内存泄漏排查详解:Chrome DevTools Memory面板、堆快照、分配时间线。

1. 内存泄漏概述

1.1 什么是内存泄漏

内存泄漏是指程序不再使用的内存未被释放,导致可用内存持续减少:

1.2 常见泄漏原因

原因示例
意外的全局变量function() { leaked = 'data' }
遗忘的定时器setInterval 未清除
闭包引用闭包持有大对象引用
DOM 引用JS 变量持有已删除 DOM 节点
事件监听器未移除的事件监听器
Map/Set 无限增长缓存未设上限
Promise处理永远 pending 的 Promise

2. Chrome DevTools Memory 面板

2.1 三种分析模式

模式用途
Heap Snapshot某时刻的内存快照
Allocation Timeline实时查看内存分配
Allocation Sampling低开销采样,查看分配

2.2 堆快照分析流程

1. 打开 DevTools → Memory → Heap snapshot
2. 拍摄初始快照(Snapshot 1)
3. 执行操作(触发可能的泄漏)
4. 拍摄第二次快照(Snapshot 2)
5. 选择 "Snapshot 2" → 视图切换为 "Comparison"
6. 按 "Delta" 排序,找出增长最多的对象

2.3 快照视图

视图说明
Summary构造函数分组
Comparison两个快照的差异
Containment对象引用关系(GC 对象
Statistics内存分布饼图

2.4 关键列含义

列名说明
Constructor构造函数
Distance到 GC 的距离
Shallow Size对象自身占用的内存
Retained Size对象及其引用占用的总内存
Delta两次快照间的变化

3. 常见泄漏排查

3.1 Detached DOM 节点

// 泄漏:引用了已从 DOM 移除的节点
const detachedNodes = [];
function createAndRemove() {
  const el = document.createElement('div');
  el.textContent = 'temporary';
  document.body.appendChild(el);
  document.body.removeChild(el);
  detachedNodes.push(el); // el 已从 DOM 移除,但被数组引用
}

排查步骤

  1. 拍摄快照
  2. 搜索框输入 Detached
  3. 查看 Detached HTMLDivElement
  4. 击查看 Retainers(引用链)

3.2 闭包泄漏

// 泄漏:闭包持有不必要的引用
function setup() {
  const hugeData = new Array(1000000).fill('x');

  return function process() {
    // 只用了 hugeData.length,但整个数组被闭包持有
    return hugeData.length;
  };
}

const fn = setup(); // hugeData 无法被 GC

排查步骤

  1. 拍摄快照
  2. 搜索 closure
  3. 展开[[Scope]]
  4. 查看闭包捕获了哪些变量

3.3 事件监听器泄漏

// 泄漏:重复添加监听器
function setup() {
  const element = document.getElementById('btn');
  element.addEventListener('click', handler); // 每次调用都添加
}

// 修复:先移除再添加
function setup() {
  const element = document.getElementById('btn');
  element.removeEventListener('click', handler);
  element.addEventListener('click', handler);
}

排查步骤

  1. DevTools → Elements → Event Listeners
  2. 查看每个元素上的监听器数量
  3. 勾选 “Ancestors” 查看所有祖先的监听器

3.4 Map/Set 无限增长

// 泄漏:缓存无限增长
const cache = new Map();

function getData(key) {
  if (!cache.has(key)) {
    cache.set(key, fetchExpensiveData(key));
  }
  return cache.get(key);
}

// 修复:使用 LRU 缓存
class LRUCache {
  constructor(maxSize = 100) {
    this.maxSize = maxSize;
    this.cache = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) return undefined;
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value); // 移到末尾
    return value;
  }

  set(key, value) {
    if (this.cache.has(key)) this.cache.delete(key);
    this.cache.set(key, value);
    if (this.cache.size > this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }
}

4. Allocation Timeline

4.1 使用方法

1. DevTools → Memory → Allocation instrumentation on timeline
2. 点击 Start
3. 执行操作
4. 点击 Stop
5. 蓝色柱状条表示仍在内存中的分配
6. 灰色柱状条表示已被 GC 回收的分配

4.2 识别泄漏模式

正常模式: ████████░░░░░░████░░░░░░░  (分配后回收)
泄漏模式: ████████████████████████  (持续增长不回收)

5. 自动化检测

5.1 Puppeteer 内存测试

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto('http://localhost:3000');

  // 测量初始内存
  const initial = await page.metrics();

  // 模拟操作
  for (let i = 0; i < 100; i++) {
    await page.click('#add-item');
    await page.waitForTimeout(100);
  }

  // 强制 GC
  await page.evaluate(() => gc?.());

  // 测量最终内存
  const final = await page.metrics();

  const growth = final.JSHeapUsedSize - initial.JSHeapUsedSize;
  console.log(`内存增长: ${(growth / 1024 / 1024).toFixed(2)} MB`);

  if (growth > 5 * 1024 * 1024) {
    console.error('可能存在内存泄漏!');
  }

  await browser.close();
})();

5.2 Node.js 内存监控

// 使用 --expose-gc 标志启动
// node --expose-gc test.js

function checkMemory() {
  global.gc();
  const used = process.memoryUsage().heapUsed / 1024 / 1024;
  console.log(`Heap used: ${used.toFixed(2)} MB`);
  return used;
}

const baseline = checkMemory();

// 执行操作
for (let i = 0; i < 10000; i++) {
  createObject();
}

const after = checkMemory();
const leak = after - baseline;
console.log(`潜在泄漏: ${leak.toFixed(2)} MB`);

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式