场景:公司最近发现在网页随着时间越用越久,越来越卡。通过chrome的任务管理器,发现内存增长的很厉害,疑似内存泄漏。通过调试后发现,多次开关tab页(使用iframe实现的)内存不会下降。下面做了一个demo还原内存泄漏。
父窗口html(命名为leak.html):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<input type="button" value="创建iframe页面" onclick="createIframe()" />
<input type="button" value="关闭iframe页面" onclick="closeIframe()" />
<div id="content" style="width:500px;height:300px;border:1px solid black"></div>
<script type="text/javascript">
var arr = new Array();
function createIframe(){
const iframe = document.createElement("iframe");
iframe.src = "iframe.html";
iframe.id = "ifr";
document.getElementById("content").appendChild(iframe);
console.log("创建iframe完成");
}
function closeIframe(){
let iframe = document.getElementById("ifr");
iframe.src = "about:blank";
iframe.contentWindow.document.write('')
iframe.contentWindow.document.clear();
iframe.contentWindow.close();
iframe.parentNode.removeChild(iframe);
iframe = null;
}
</script>
</body>
</html>
iframe页(命名为:iframe.html,在该页面定义了一个大内存变量):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
iframe page
<input type="button" value="父窗口赋值" onclick="assignment()" >
<script>
//创建一个大对象
var a = new Array(1024*1024*10).join('0').split('');
function assignment(){
//将iframe中的变量赋值给父窗口变量
const obj = {name:"test"};//变量为json对象时不释放内存
//const str = "test";//变量为字符串或者其他常量,会释放内存
/* const fun = function(){
console.log("test");//变量为函数时不会释放内存
} */
//const obj2 = new Array();//变量为对象不会释放内存
window.parent.arr.push(obj);
console.log("父窗口赋值完成");
}
</script>
</body>
</html>
将上面两段代码拷贝到demo项目中,按名称命名后运行。打开leak.html页面如下:
多次执行创建iframe页面,父窗口赋值,关闭iframe页面。观察浏览器内存变化。
观察方式:打开chrome控制台观察Performance monitor中的JS heap size的值
或者打开chrome任务管理器观察:
可以每次在关闭iframe页后执行垃圾回收,再观察内存,确保结果准确性。
经过观察发现,多次开关iframe页内存持续增长不会下降
结论:
如果父窗口(外部窗口)保留了iframe中的引用变量、方法或者父窗口将内部方法绑定到父窗口,即使删除iframe也不会释放iframe中所有的js堆内存和dom所占用的内存
1、如果在iframe中使用window.top给父窗口赋值
window.top.innerObject = someInsideObject
2、如果在父窗口使用iframeEl.contentWindow将iframe中的变量赋值给父窗口的变量
innerObject = iframeEl.contentWindow.someInsideObject
3、如果将iframe中的方法绑定到父窗口上
window.top.document.addEventLister('click',function(){...});
以上这些操作都将导致删除iframe不释放内存
解决方法:
断开所有父页面对iframe中的变量的引用
上述demo中新建一个断开引用的方法如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<input type="button" value="创建iframe页面" onclick="createIframe()" />
<input type="button" value="关闭iframe页面" onclick="closeIframe()" />
<input type="button" value="断开ifame变量的引用" onclick="shutIframe()" />
<div id="content" style="width:500px;height:300px;border:1px solid black"></div>
<script type="text/javascript">
var arr = new Array();
function createIframe(){
const iframe = document.createElement("iframe");
iframe.src = "iframe.html";
iframe.id = "ifr";
document.getElementById("content").appendChild(iframe);
console.log("创建iframe完成");
}
function closeIframe(){
let iframe = document.getElementById("ifr");
iframe.src = "about:blank";
iframe.contentWindow.document.write('')
iframe.contentWindow.document.clear();
iframe.contentWindow.close();
iframe.parentNode.removeChild(iframe);
iframe = null;
}
//断开iframe中变量的引用
function shutIframe(){
for(let i = 0;i < arr.length;i++){
arr[i] = null;
}
console.log("断开引用成功");
}
</script>
</body>
</html>
将上述代码替换demo中的leak.html代码。打开页面如下:
在多次开关ifame后,再点击断开ifame变量引用的按钮,手动执行垃圾回收后内存恢复正常。注意:不要在chrome控制台操作变量将引用断开,如果这么做内存不会释放。个人理解原因是控制台又保留了一份变量。
第二种方式:使用postMessage在父子页面进行传值。尽量不要使用window.top或iframeEl.contentWindow直接操作父子窗口中对变量或方法具体用法参考:
https://blog.csdn.net/tang_yi_/article/details/79401280
这个方法传值,不能传递方法。操作机制是将窗口中对变量进行克隆后再传输。
第三种方式:解除父窗口对iframe中的事件绑定
对这个问题对个人理解:
变量或方法在定义时就确定了全局上下文,所以将值传递给父窗口时,即使删除iframe,因为变量或方法的引用不会释放ifame中的上下文。又因为基本数据类型是值传递,如果父窗口引用的是子窗口的基本数据类型的变量,不会导致内存泄漏(纯属个人理解,如有不正确的地方请多指教)。
tips:
判断引用是否为当前上下文创建。将以下代码直接拷贝到控制台执行
function scan(o) {
try{
Object.keys(o).forEach(function (key) {
var val = o[key];
if (typeof val !== 'string' && typeof val !== 'number' && typeof val !== 'boolean' && !(val instanceof Object) && val !== null && val !== undefined) {
//debugger;
console.log("obj--"+o+"objkey--"+key);
}else{
}
})
}catch(err){
}
}
scan(window)
如果非当前上下文的引用将被输出(只是window中的引用)。原理是使用instanceof Object判断来自子窗口的引用返回false。如果将Object替换为iframeEl.contentWindow.Object则返回true。
js其他内存泄漏方式参考如下链接:
https://segmentfault.com/a/1190000011842980
https://www.tutorialspoint.com/how-can-detached-dom-elements-cause-memory-leak-in-javascript
https://community.pega.com/knowledgebase/articles/common-javascript-memory-leak-patterns
https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/