前端应用的内存泄漏是单页应用(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` 中,导致数组持续增长。- <button onclick="myClick()">执行fn1函数</button>
- <script>
- function fn1 () {
- let a = new Array(10000); // 大数组
- let b = 3;
- function fn2() {
- let c = [1, 2, 3];
- }
- fn2();
- return a;
- }
- let res = [];
- function myClick() {
- res.push(fn1());
- }
- </script>
复制代码 使用 Performance 录制,先手动触发一次垃圾回收确立基准线,然后点击几次按钮,最后再次触发垃圾回收。可见 JS Heap 曲线呈阶梯式上升,最终高度远高于基准线,说明存在泄漏。再切换到 Memory 面板,用 Allocation instrumentation on timeline 录制,每次点击后出现蓝色柱形,触发垃圾回收后蓝色柱形未变灰,确认泄漏。最后用 Heap snapshot 对比快照,发现新增了大量 Array 对象,并可查看具体数据。
2. 全局变量(未声明变量)- function fn1() {
- // 变量name未被声明,自动挂载到window
- name = new Array(99999999);
- }
- fn1();
复制代码 解决方案:使用严格模式(`"use strict"`),或在赋值前先声明变量。
3. 分离的 DOM 节点
以下代码点击按钮移除子节点,但全局变量 `child` 仍持有该节点引用,导致内存无法释放。- <div id="root">
- <div class="child">我是子元素</div>
- <button>移除</button>
- </div>
- <script>
- let btn = document.querySelector('button');
- let child = document.querySelector('.child');
- let root = document.querySelector('#root');
- btn.addEventListener('click', function() {
- root.removeChild(child);
- });
- </script>
复制代码 用 Memory 快照对比,在快照的筛选框中输入 `detached`,可看到未被释放的分离节点。修复方法是将对 `child` 的引用移到回调内部,退出后自动释放:- <div id="root">
- <div class="child">我是子元素</div>
- <button>移除</button>
- </div>
- <script>
- let btn = document.querySelector('button');
- btn.addEventListener('click', function() {
- let child = document.querySelector('.child');
- let root = document.querySelector('#root');
- root.removeChild(child);
- });
- </script>
复制代码 再次检查快照,不再出现 detached 节点。
4. 控制台打印导致对象无法回收- <button>按钮</button>
- <script>
- document.querySelector('button').addEventListener('click', function() {
- let obj = new Array(1000000);
- console.log(obj);
- });
- </script>
复制代码 Performance 录制显示每次点击后内存阶梯上升,且垃圾回收后无法降到基准线。因为 `console.log` 会保持对对象的引用(浏览器内部保留打印对象的快照)。注释掉 `console.log` 后,内存即可正常回收。
5. 遗忘的定时器- <button>开启定时器</button>
- <script>
- function fn1() {
- let largeObj = new Array(100000);
- setInterval(() => {
- let myObj = largeObj;
- }, 1000);
- }
- document.querySelector('button').addEventListener('click', function() {
- fn1();
- });
- </script>
复制代码 点击按钮后,`setInterval` 的回调函数引用了 `largeObj`,导致该大数组无法被回收。在 Performance 中可见内存不降,Allocation 图中蓝色柱形一直存在。修复方式:在不需要定时器时调用 `clearInterval`。例如设定只执行三次:- <button>开启定时器</button>
- <script>
- function fn1() {
- let largeObj = new Array(100000);
- let index = 0;
- let timer = setInterval(() => {
- if(index === 3) clearInterval(timer);
- let myObj = largeObj;
- index++;
- }, 1000);
- }
- document.querySelector('button').addEventListener('click', function() {
- fn1();
- });
- </script>
复制代码 再次录制可看到,3秒后蓝色柱形变灰,内存被释放。
四、总结
内存泄漏排查的关键在于:熟悉常见泄漏场景,掌握 Chrome DevTools 的 Performance(确认泄漏存在)和 Memory(定位泄漏对象)两个面板的配合使用。对于闭包、全局变量、DOM 引用、定时器等典型场景,本文均给出了可复现的示例及修复方案。在实际项目中,建议在关键操作(如路由切换、弹窗关闭)前后主动拍摄堆快照对比,并结合 `Allocation instrumentation on timeline` 观察内存分配是否回收,从而快速定位问题。 |