← 返回题目列表

前端常见内存泄漏有哪些?如何排查?

高频 困难 第 17 / 29 题 更新于 2026/07/27
JavaScript内存泄漏性能优化

简化版

前端内存泄漏是对象已经不再需要,却仍被引用导致垃圾回收无法释放。常见原因有未清除定时器、未解绑事件、闭包长期引用大对象、全局变量、缓存无限增长、DOM 被移除但 JS 仍引用。

详细版

常见场景:

  • setInterval 没有 clearInterval
  • 组件卸载后没有移除 addEventListener
  • 闭包保存了大数组或 DOM。
  • 全局 Map/数组一直追加数据。
  • Detached DOM:DOM 离开页面,但仍被 JS 变量引用。

排查方法:

  • Chrome DevTools Memory 拍 Heap Snapshot。
  • 使用 Performance 观察内存曲线。
  • 反复进入/离开页面,看对象数量是否持续增长。
  • 检查组件卸载钩子是否清理副作用。

预防思路是:谁创建副作用,谁负责清理。

完整版教学

一、垃圾回收看的是可达性

JavaScript 垃圾回收通常基于可达性。从全局对象、当前调用栈、闭包引用等根出发,能访问到的对象就不会被释放。

所以内存泄漏不是对象“大”,而是对象“不该活还活着”。对象之间即使形成环,只要整个环从 GC Roots 不可达也可以被回收;全局变量、调用栈和宿主保存的监听器等根路径才是分析重点。

二、定时器和事件监听最常见

组件里启动定时器:

const timer = setInterval(update, 1000);

如果组件销毁后不清理,定时器回调仍然存在,并可能引用组件状态。

事件监听类似:

window.addEventListener('resize', onResize);

卸载时必须:

window.removeEventListener('resize', onResize);

三、闭包引用不是错,长期引用才危险

闭包会保留外层变量。如果外层变量是大型数据或 DOM 节点,而闭包被全局事件、定时器、缓存长期持有,就可能造成泄漏。

解决方式是让生命周期清晰:不用时解绑、置空、清缓存。置空局部变量只有在它确实切断最后一条业务强引用时才有效,若 window 监听器仍持有同一闭包,单独改另一个变量不会解决问题。

四、如何用 DevTools 排查

一个常见方法是:打开 Memory,进入页面拍一次快照,离开页面后强制 GC 再拍一次,多次重复。如果某类对象数量持续增长,说明可能被引用链保住。

Heap Snapshot 里可以看 Retainers,找到是谁引用了这些对象。这个引用链比猜代码更可靠。快照本身也会带来分析开销,并且单次结果容易混入正常缓存,所以应在可重复操作和相同 GC 条件下做多轮对比。

五、面试追问与工程落地

内存泄漏面试常追问“闭包、定时器、事件哪个最危险”。它们本身都不危险,危险的是生命周期不受控。比如 SPA 页面切走后,window 上的 resize 监听还在,回调闭包引用了组件状态,这才是典型泄漏链路。

还可能追问 WeakMap 的作用。WeakMap 的 key 是弱引用,如果外部没有其他引用指向 key 对象,垃圾回收可以回收它。它适合存 DOM 或对象实例的附加元数据,避免缓存结构阻止对象释放。

工程排查要看趋势,不要看到内存上升就下结论。浏览器为了性能不会立刻 GC,短时间上升正常。关键是反复进入离开页面并强制 GC 后,相关对象是否持续增长,Retainers 里是否有不该存在的引用链。

六、用保留路径而不是文件大小判断泄漏

假设进入详情页前强制 GC 后堆占用 30MB,进入后为 48MB,离开并再次强制 GC 后回到 32MB,这更像正常分配与缓存;若重复 5 次后基线依次为 32、40、48、56、64MB,且每轮多出约 8MB 的同类组件对象,就值得沿 Retainers 查找稳定增长的引用链。

Window(GC Root)
└─ resize listener
   └─ callback closure
      └─ component state
         └─ detached DOM subtree
表象是否必然泄漏验证方式
内存短时上涨等待或强制 GC 后比较基线
Detached DOM 出现看是否仍有不应存在的强引用
对象数量每轮净增长高风险对比多轮快照与 Retainers
Map 缓存持续增大取决于设计检查容量、淘汰和键生命周期
WeakMap 存元数据不阻止 key 因该映射而存活仍需检查其他强引用

垃圾回收时间不可预测,规范也不保证某个不可达对象一定在特定时刻被回收。因此不能把 FinalizationRegistry 当资源释放钩子;数据库连接、事件监听、Observer 和定时器仍应由确定性的 dispose 或卸载逻辑清理。

排查顺序是“复现稳定增长 → 强制 GC 排除暂存 → 找增长对象 → 沿 Retainers 回到 GC Root”,而不是看到任务管理器曲线上升就猜闭包泄漏。

七、常见误区与追问

  • 误区:内存占用上升就等于内存泄漏。 引擎可能延迟 GC、保留堆空间或缓存资源,关键是清理生命周期后基线是否持续增长。
  • 误区:闭包天然会造成泄漏。 闭包只建立引用关系;持有它的根引用超期存在并保留无用对象时才构成业务泄漏。
  • 误区:把缓存从 Map 改成 WeakMap 就一定修好泄漏。 WeakMap 只避免映射本身强持有 key,value、其他强引用和无界业务数据仍可能泄漏。
  • 追问:为什么移除 DOM 后对象仍在快照里? JavaScript 变量、监听器、Observer 或框架缓存可能仍引用节点,使其成为 detached 但可达的子树。
  • 追问:removeEventListener 为什么有时没有效果? 移除时必须提供匹配的事件类型、回调身份及 capture 值;重新创建的匿名函数不是原监听器。
  • 追问:FinalizationRegistry 能否保证执行清理? 不能,回收和清理回调都没有确定时机,宿主甚至可能永不调用,不能承担关键资源释放。
  • 追问:怎样为缓存设计生命周期? 明确容量上限、TTL 或 LRU 淘汰,提供主动清空入口,并让组件卸载或用户切换触发对应清理。

八、加强记忆

内存泄漏可以记成“对象已经下班,但引用还给它打卡”。排查时别只看代码,要看引用链;预防时记住:定时器、事件、订阅、缓存、闭包都要有清理策略。