先说一下结论:内存泄漏是前端性能优化中的常见问题,而Dom泄漏则是内存泄漏的重要途径和方式,但是在业务中的大多数场景下,Dom泄漏其实几乎是一个不可解问题,或者说你可能需要花费大量的精力去维护一个脆弱不堪的系统
可以先看一下这篇很好的文章使用Chrome开发者工具分析内存泄漏 ,初步学习一下浏览器的内存泄漏面板🍞如何使用以及内存泄漏的相关术语
简单的Demo
试一下下面这个简单demo
打开内存分析面板,然后执行如下操作,点击创建列表---创建并泄漏一个与列表节点----卸载子树。
你就会发现,你仅仅只是不小心泄漏了一个不在列表中的简单节点,却最终会导致整个子树的全部泄漏,并且点几次全部泄漏几次
内存分析面板,其中的Detach div代表着不在dom树🌲中的dom节点,也就是泄漏的dom节点
部分构造函数说明 |
---|
(compiled code) --- 编译代码占用Detached - div 不处于dom树内的dom节点,代表泄漏的dom,是内存分析的重点 |
system / Context调用一个函数所需要的潜在对象,例如闭包使用的实际数据InternalNode此类别表示在 V8 之外分配的对象,例如 Blink 定义的 C++ 对象。 |
(closure) --- 闭包(array) |
Object --- JS对象类型(system) |
(string) --- 字符串类型,有时对象里添加了新属性,属性的名称也会出现在这里 |
Array --- JS数组类型cls Window --- JS的window对象 |
Tabs/Route 灾难
上面那个例子🌰可能不够震撼,来看看这个
业务中常见的tabs切换,稍微点几次切换,打开堆内存快照
JSX
import { Tabs } from "antd";
import React, { useEffect, useRef } from "react";
import { Avatar, List } from "antd";
const dataBase = [
{
title: "Ant Design Title 1",
},
{
title: "Ant Design Title 2",
},
{
title: "Ant Design Title 3",
},
{
title: "Ant Design Title 4",
},
];
const data = new Array(20).fill(0).reduce((pre, now) => {
return [...pre, ...dataBase];
}, [] as any[]);
const ListDemo: React.FC = () => {
const ref = useRef<HTMLAnchorElement>(null);
useEffect(() => {
const el = ref.current;
if (el) {
//模拟意外泄漏
document.addEventListener("visibilitychange", (e) => {
if (el && document.visibilityState === "visible") {
el.classList.add("active");
}
});
}
}, []);
useEffect(() => {
//do something
return () => {
//clear
};
});
return (
<div>
<List
itemLayout="horizontal"
dataSource={data}
renderItem={(item: any, index) => {
return (
<List.Item>
<List.Item.Meta
avatar={
<Avatar
src={`https://api.dicebear.com/7.x/miniavs/svg?seed=${index}`}
/>
}
title={<a href="https://ant.design">{item.title}</a>}
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
/>
</List.Item>
);
}}
/>
<div>
<div>
<a href="" className="leak-dom" ref={ref}>
泄漏dom
</a>
</div>
</div>
</div>
);
};
//page leak
const App: React.FC = () => (
<Tabs defaultActiveKey="1" destroyInactiveTabPane>
<Tabs.TabPane tab="Tab 1" key="1">
<ListDemo />
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 2" key="2">
<ListDemo />
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 3" key="3">
<ListDemo />
</Tabs.TabPane>
</Tabs>
);
export default App;
这dom啊,你一点,诶---------它全部漏了,pane级别的哦😯
实际生产环境的例子🌰
上面的举例可能有些强行,我们🉑一看看被带到实际线上环境的例子🌰
打开掘金创作者中心 juejin.cn/creator/gro... 在数据中心,创作成长等tabs页随意切换多次
录制快照,可以在录制⏺️之前点击几次垃圾回收♻️
此处可以@掘金官方,你也可以去查看任意网站的会destroy的Tabs上去试试,大概率都会复刻
为什么会这样?
原理其实很简单,任何一个dom上都存在父,子,前后等一长串的引用链,任意一个微小的dom节点就可以catch住整个子树🌲(e.target也同理)
如何解决?
根据上述描述,应该可以清楚这根本不是一个可解的问题,或许你花了大量的精力去解决了现在的Dom泄漏问题,但后续的任意一个小迭代都能让你的所有成果付之一炬,甚至有些问题是第三方库带来的,根本无从下手。我们只能从某些方面去尽量避免它
非必要不删除
既然被删除的dom几乎一定会被泄漏,那么直接不删除就行,比如Tabs存在高频切换的场景就不要destroy
保持对dom的间接引用
通过useRef或document.querySelector的间接引用机制,永远防止直接创建对dom的引用路径(不过这种方式更多是一种编程习惯,解决不了上述问题)
JSX
useEffect(() => {
if (ref.current) {
//el的简写写在回调函数内,不要写在外部,保持对dom的间接引用,这样稳定不会发生dom泄漏
const el = ref.current;
document.body.addEventListener("click", (e) => {
//const el = ref.current;
if (el) {
console.log(el.id);
}
});
}
}, []);
querySelector形式
JSX
useEffect(() => {
if (ref.current) {
//模拟意外泄漏
document.body.addEventListener("click", (e) => {
//querySelector保持间接引用
const el = document.querySelector(".el");
console.log(el?.classList);
});
}
}, []);
Iframe内存隔离
预览页等大型Dom的Drawer或Modal,无法采用不摧毁方案,每次关闭-新建都会导致页面级Dom泄漏,由于内存泄漏原理是变量具有从全局对象到变量的路径,因此通过Iframe的隔离方案,实际上也能解决内存隔离的问题
javascript
参考文档
参考文档