前言
现代前端开发中,随着应用的复杂性和交互性的增加,OOM(Out Of Memory,内存不足)问题和内存泄漏逐渐成为影响用户体验和应用性能的关键挑战。排查和解决这些问题需要开发人员具备良好的调试技巧和优化策略。
造成OOM的一些原因
1、 未销毁的事件监听器
事件监听器是常见的内存泄漏源。当你在DOM元素上添加事件监听器时,如果不手动删除它们,它们将一直存在于内存中,即使元素被销毁了。
// 内存泄漏示例
const button = document.getElementById('myButton');
button.addEventListener('click', () => {// 处理点击事件
});
解决方法:在组件卸载或不再需要时,务必记得删除事件监听器。
// 解决内存泄漏
const button = document.getElementById('myButton');
const handleClick = () => {// 处理点击事件
};
button.addEventListener('click', handleClick);// 在组件卸载或不再需要时,删除事件监听器
button.removeEventListener('click', handleClick);
2、 引用计数循环
循环引用是另一个常见的内存泄漏源。当两个或多个对象相互引用时,并且没有任何引用指向它们之中的任何一个时,它们将无法被垃圾回收。
// 内存泄漏示例
function createObjects() {const obj1 = {};const obj2 = {};obj1.ref = obj2;obj2.ref = obj1;
}
createObjects();
解决方法:避免循环引用,或者在不再需要这些引用时手动解除它们。
// 解决内存泄漏
function createObjects() {const obj1 = {};const obj2 = {};obj1.ref = obj2;obj2.ref = obj1;// 不再需要 obj1 和 obj2 的引用时,将它们设为 nullobj1.ref = null;obj2.ref = null;
}
createObjects();
3、未清理的定时器和回调
使用 setInterval
或 setTimeout
来执行循环或延时操作时,如果忘记清理它们,可能导致持续的内存占用。
// 内存泄漏示例
const intervalId = setInterval(() => { // 执行重复任务 }, 1000);
解决方法:在组件卸载或不再需要定时器时,清除它们。
// 解决内存泄漏
const intervalId = setInterval(() => {// 执行重复任务
}, 1000);// 在组件卸载或不再需要时,清除定时器
clearInterval(intervalId);
4、未清理的全局变量
使用全局变量保存大量数据可能导致内存过度使用,因为这些变量不会自动释放。
// 内存泄漏示例
let dataCache = fetchData();
解决方法:使用局部变量或尽可能减少全局变量的使用,确保在不需要时明确清理。
// 解决内存泄漏
function handleData() {const dataCache = fetchData();// 使用完后清理processData(dataCache);
}
5、未释放的 DOM 元素
创建并打算随时间动态更新的 DOM 元素可能会造成内存问题,如果这些元素在不再需要时没有被移除。
// 内存泄漏示例
const element = document.createElement('div');
document.body.appendChild(element);
// 未移除时此元素可能一直占用内存
解决方法:管理 DOM 元素的生命周期,确保在设备卸载时移除不必要的元素。
// 解决内存泄漏
const element = document.createElement('div');
document.body.appendChild(element);
// 不再需要时移除
document.body.removeChild(element);
6、未清理的闭包
JavaScript 中的闭包允许函数访问外部作用域,但如果这些闭包长期存在并引用大量数据会导致内存泄漏。
// 内存泄漏示例
function createClosure() {const largeObject = new Array(10000).fill('memory-leak');return function() {console.log(largeObject);};
}
const closureFn = createClosure();
解决方法:确保在不再需要这些闭包时,声明周期结束时移除相关引用。
// 解决内存泄漏
function createClosure() {const largeObject = new Array(10000).fill('memory-leak');const closureFn = () => console.log(largeObject);// 用完后尽可能清理引用return closureFn();
}
排查和定位
1、chrome performance观察GC前后视图
利用performance在页面一次渲染后执行gc,观察渲染前和渲染后gc的内存占用情况,可以判断应用是否存在oom
的情况。
首先打开Chrome Devtool
开发者工具,点击进入到 Performance
面板,勾选上 Screenshots
及 Memory
选项,点击箭头所指的 record
按钮开始记录页面参数信息,在此过程中可以进行一些内存泄漏相关的可疑操作,方便后续的分析。
录制中执行一次gc,观察两段线的高度即可,如果会出现递增的情况,则可推断页面存在内存泄漏,可进行代码排查。
2、更精准的定位——chrome memory观察接近时间段的内存占用情况
通过chrome提供的内存快照可实时观测应用的内存占用情况。
我们在测试的应用中插入一段造成OOM
的代码:
import React, { useEffect, useState } from 'react';const MemoryLeakTest: React.FC = () => {const [data11111, setData11111] = useState<string[]>([]);const [counter, setCounter] = useState<number>(0);useEffect(() => {let intervalId: ReturnType<typeof setInterval>;// 模拟持续不断地增加数据intervalId = setInterval(() => {const newData = Array.from({ length: 100000 }, () => Math.random().toString());setData11111(prevData => [...prevData, ...newData]);setCounter(counter + 1);}, 100); // 每100毫秒添加一批新数据// 不执行清理函数以模拟内存泄露return () => {// clearInterval(intervalId); // 注释掉清理函数};}, [counter]); // 注意这里的依赖项,确保每次计数器变化都会重新设置定时器
随后进行三次快照,对比排查,发现每一次的快照内存占用都会线性递增,在Delta
列降序排列,第一条就可以找到罪魁祸首。仔细顺着堆内存栈往下排查,能找到罪魁祸首setData11111
触发了多次更新。
结尾
本文以OOM
为题,梳理了开发中会造成内存泄漏的情况、最后基于chrome performance、memory两个工具进行OOM
排查定位分析,我们在日常开发中也需要在每次迭代后回归核心页面的基本性能,防止不必要的线上客诉。