跳到主要内容

JS 内存泄漏如何检测?常见场景有哪些?

面试速答(30 秒版 TL;DR)

  • JS 有 GC,但“对象是否能被回收”取决于 可达性:只要还有引用链能到达,就不会回收。
  • 检测(浏览器端)常用 Chrome DevTools:
    • Performance 看长时间交互后的内存曲线是否持续上升
    • Memory 做 Heap Snapshot 对比、看 Detached DOM、看 Retainers(是谁把它留住的)
    • Performance monitor 观察 JS heap size、DOM Nodes
  • 常见泄漏来源:未解绑事件监听、定时器未清理、Detached DOM、全局缓存无限增长、闭包意外持有大对象。

心智模型:泄漏 = 生命周期管理失败

面试回答抓住一句话:不是 GC 不工作,而是你的引用让它没法回收


浏览器端:一套可复现的排查流程

1)先判断“是不是泄漏”

  • 打开 DevTools 的 Performance/Performance monitor
  • 重复执行同一用户操作(打开弹窗、切换路由、滚动列表等)
  • 如果 JS heap / DOM Nodes 持续上升且不回落,才怀疑泄漏

2)用 Heap Snapshot 找“谁在持有”

操作要点(面试可口述):

  • 在可疑操作前后各拍一次 Heap Snapshot
  • 对比两次快照,关注增长明显的构造类型(Array、Object、Closure、Detached DOM)
  • 点开对象看 Retainers:沿着引用链往上找,定位到具体代码路径(监听器、缓存、全局单例等)

3)关注 Detached DOM

常见模式:

  • DOM 节点从页面移除了,但仍被 JS 引用(例如数组里缓存了节点,或事件回调闭包引用了节点)

常见泄漏场景(高频背诵点)

1)事件监听不解绑

function mount() {
const onResize = () => {/* ... */};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}

要点:解绑需要同一个函数引用。

2)setInterval / 定时器没清理

const id = setInterval(() => {/* ... */}, 1000);
// 组件卸载/页面离开时:clearInterval(id)

3)缓存无限增长(Map/数组)

  • 例如把请求结果按 key 缓存,但没有 TTL/上限/淘汰策略。

4)Detached DOM

  • 典型:把 DOM 节点塞进全局数组,或闭包捕获了节点引用导致无法回收。

5)闭包持有大对象

  • 不是闭包必然泄漏,而是闭包“活得太久”且捕获了不该捕获的数据。

典型追问

Q1:怎么区分“正常上涨”和“泄漏”?

  • 正常:操作峰值上升,但 idle 后会回落或趋于稳定。
  • 泄漏:重复同一操作,基线持续上升且不回落。

Q2:定位到代码后怎么修?

  • 缩短引用生命周期:解绑监听、清理定时器、释放缓存、断开对 DOM 的引用。
  • 给缓存加上限/淘汰策略(LRU/TTL 等),避免无界增长。

易错点/坑

  • 只看一次 snapshot 就下结论:要做“前后对比”。
  • 误把“内存抖动”当泄漏:关键看基线是否持续上升。

速记要点(可背诵)

  • 泄漏本质:引用让对象一直可达。
  • 三板斧:看曲线 -> 两次快照对比 -> Retainers 找持有者。
  • 高频元凶:事件、定时器、Detached DOM、无界缓存、长寿命闭包。