查看: 99|回复: 1

Chrome DevTools内存泄漏排查实战:Performance与Memory面板定位

[复制链接]
发表于 3 小时前 | 显示全部楼层 |阅读模式
前端应用的内存泄漏是单页应用(SPA)中常见的性能隐患。不再使用的内存未被释放,会导致页面占用持续增长,轻则卡顿、加载缓慢,重则浏览器崩溃。尤其是在长时间运行的后台管理系统中,路由切换频繁却无法回收内存,用户操作响应会越来越迟钝,最终只能强制刷新。本文将围绕实际开发中典型的内存泄漏场景,手把手演示如何使用 Chrome DevTools 的 Performance 和 Memory 面板进行定位与修复。

一、常见内存泄漏场景
1. 意外的全局变量:未声明的变量(如 `a = 10` 而非 `let a = 10`)会挂载到 `window` 上,页面不刷新就不会释放。
2. 闭包滥用:闭包保留外部作用域的引用,尤其是长期持有 DOM 或大型对象时,内存无法回收。
3. 未清理的 DOM 引用:删除 DOM 节点后,仍存在对其的引用(如 `let el = document.getElementById('test')`,删除节点后未置 `null`)。
4. 定时器/事件监听未销毁:`setInterval` 未用 `clearInterval` 清除,`addEventListener` 未用 `removeEventListener` 解绑,尤其在组件挂载/卸载时容易遗漏。
5. 第三方库/插件残留:图表库、播放器等使用后未调用销毁方法。
6. 数组/对象无限增长:全局缓存对象未设置过期机制,或数组持续 push 数据但未清理无效项。

二、核心工具:Chrome DevTools
第一步:复现内存泄漏行为
稳定复现才能有效定位。典型操作包括:
- 路由切换(SPA 中反复切换 A→B→A→B);
- 点击按钮触发特定操作(如打开弹窗后关闭);
- 长时间滚动或轮询请求。

第二步:使用 Performance 面板确认泄漏
1. 打开 Chrome 开发者工具,切换到「Performance」面板。
2. 点击左上角的「录制」按钮(圆形红点),然后执行复现步骤(如切换路由 5 次)。
3. 执行完成后点击「停止」,等待面板生成报告。
4. 观察 JS Heap 曲线:若曲线持续上升且不回落(即使操作停止后内存仍未下降),则大概率存在泄漏;若曲线在操作后能回落至初始水平,说明内存正常回收。

第三步:使用 Memory 面板定位泄漏对象
Memory 面板支持三种快照类型:
- Heap snapshot(堆快照):捕获当前内存中所有对象的快照,可对比不同快照的差异。
- Allocation instrumentation on timeline(时间线分配记录):实时记录内存分配过程,适合观察某操作期间的内存分配细节。
- Allocation sampling(分配采样):低开销的内存采样,适合快速排查大面积泄漏。

堆快照对比操作:
1. 打开 DevTools → 切换到「Memory」面板。
2. 选择「Heap snapshot」,点击「Take snapshot」拍摄初始快照(快照1,未执行任何操作)。
3. 执行一次泄漏复现步骤(如切换一次路由)。
4. 再次点击「Take snapshot」拍摄快照2。
5. 重复复现步骤 N 次(如再切换 3 次路由),拍摄快照3。
6. 在快照列表中选择快照2或快照3,下拉框选择「Comparison」(对比),并选择“与快照1对比”。此时可看到哪些对象新增了内存占用,从而定位泄漏来源。

另外,Allocation instrumentation on timeline 可以用更直观的柱形图展示内存分配:蓝色柱形表示当前时间线占用的内存,灰色柱形表示已释放的内存。若操作后蓝色柱形未变为灰色,说明存在泄漏。

三、示例代码与排查实战
1. 闭包使用不当引起内存泄漏
以下代码中,每次点击按钮都会将 `fn1` 函数的返回值(一个大数组)添加到全局数组 `res` 中,导致数组持续增长。
  1. <button onclick="myClick()">执行fn1函数</button>
  2. <script>
  3. function fn1 () {
  4.     let a = new Array(10000); // 大数组
  5.     let b = 3;
  6.     function fn2() {
  7.         let c = [1, 2, 3];
  8.     }
  9.     fn2();
  10.     return a;
  11. }
  12. let res = [];
  13. function myClick() {
  14.     res.push(fn1());
  15. }
  16. </script>
复制代码
使用 Performance 录制,先手动触发一次垃圾回收确立基准线,然后点击几次按钮,最后再次触发垃圾回收。可见 JS Heap 曲线呈阶梯式上升,最终高度远高于基准线,说明存在泄漏。再切换到 Memory 面板,用 Allocation instrumentation on timeline 录制,每次点击后出现蓝色柱形,触发垃圾回收后蓝色柱形未变灰,确认泄漏。最后用 Heap snapshot 对比快照,发现新增了大量 Array 对象,并可查看具体数据。

2. 全局变量(未声明变量)
  1. function fn1() {
  2.     // 变量name未被声明,自动挂载到window
  3.     name = new Array(99999999);
  4. }
  5. fn1();
复制代码
解决方案:使用严格模式(`"use strict"`),或在赋值前先声明变量。

3. 分离的 DOM 节点
以下代码点击按钮移除子节点,但全局变量 `child` 仍持有该节点引用,导致内存无法释放。
  1. <div id="root">
  2.     <div class="child">我是子元素</div>
  3.     <button>移除</button>
  4. </div>
  5. <script>
  6. let btn = document.querySelector('button');
  7. let child = document.querySelector('.child');
  8. let root = document.querySelector('#root');
  9. btn.addEventListener('click', function() {
  10.     root.removeChild(child);
  11. });
  12. </script>
复制代码
用 Memory 快照对比,在快照的筛选框中输入 `detached`,可看到未被释放的分离节点。修复方法是将对 `child` 的引用移到回调内部,退出后自动释放:
  1. <div id="root">
  2.     <div class="child">我是子元素</div>
  3.     <button>移除</button>
  4. </div>
  5. <script>
  6. let btn = document.querySelector('button');
  7. btn.addEventListener('click', function() {
  8.     let child = document.querySelector('.child');
  9.     let root = document.querySelector('#root');
  10.     root.removeChild(child);
  11. });
  12. </script>
复制代码
再次检查快照,不再出现 detached 节点。

4. 控制台打印导致对象无法回收
  1. <button>按钮</button>
  2. <script>
  3. document.querySelector('button').addEventListener('click', function() {
  4.     let obj = new Array(1000000);
  5.     console.log(obj);
  6. });
  7. </script>
复制代码
Performance 录制显示每次点击后内存阶梯上升,且垃圾回收后无法降到基准线。因为 `console.log` 会保持对对象的引用(浏览器内部保留打印对象的快照)。注释掉 `console.log` 后,内存即可正常回收。

5. 遗忘的定时器
  1. <button>开启定时器</button>
  2. <script>
  3. function fn1() {
  4.     let largeObj = new Array(100000);
  5.     setInterval(() => {
  6.         let myObj = largeObj;
  7.     }, 1000);
  8. }
  9. document.querySelector('button').addEventListener('click', function() {
  10.     fn1();
  11. });
  12. </script>
复制代码
点击按钮后,`setInterval` 的回调函数引用了 `largeObj`,导致该大数组无法被回收。在 Performance 中可见内存不降,Allocation 图中蓝色柱形一直存在。修复方式:在不需要定时器时调用 `clearInterval`。例如设定只执行三次:
  1. <button>开启定时器</button>
  2. <script>
  3. function fn1() {
  4.     let largeObj = new Array(100000);
  5.     let index = 0;
  6.     let timer = setInterval(() => {
  7.         if(index === 3) clearInterval(timer);
  8.         let myObj = largeObj;
  9.         index++;
  10.     }, 1000);
  11. }
  12. document.querySelector('button').addEventListener('click', function() {
  13.     fn1();
  14. });
  15. </script>
复制代码
再次录制可看到,3秒后蓝色柱形变灰,内存被释放。

四、总结
内存泄漏排查的关键在于:熟悉常见泄漏场景,掌握 Chrome DevTools 的 Performance(确认泄漏存在)和 Memory(定位泄漏对象)两个面板的配合使用。对于闭包、全局变量、DOM 引用、定时器等典型场景,本文均给出了可复现的示例及修复方案。在实际项目中,建议在关键操作(如路由切换、弹窗关闭)前后主动拍摄堆快照对比,并结合 `Allocation instrumentation on timeline` 观察内存分配是否回收,从而快速定位问题。
回复

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: Chrome DevTools内存泄漏排查实战:Performance与Memory面板定位

非常详实的实战指南!感谢分享。文中从常见泄漏场景到工具使用再到具体代码示例,层层递进,很实用。尤其喜欢“分离DOM节点”那个例子——很多新手容易忽略全局引用导致节点无法回收。另外,把Performance的JS Heap曲线和Memory的堆快照对比结合起来判断泄漏,这个思路清晰明了。我之前排查一个长时间轮询页面也遇到过类似问题,当时就是靠“Allocation instrumentation on timeline”看到蓝色柱形不消失才锁定了定时器未清理。要是能再聊聊WeakMap/WeakSet在解决闭包引用上的应用,就更完整了。再次感谢,收藏了!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-11 21:06 , Processed in 0.032440 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部