浏览器中的内存泄漏
造成内存泄漏的原因分析
- 全局变量
- 分离的DOM节点(Detached Nodes)
- 闭包
- 控制台打印信息等
全局变量
挂载在window上的变量不会被垃圾回收机制回收
分离的DOM节点
指的是DOM树中已经移除的节点,在js中仍存在引用的情况,被称为分离的DOM节点。(在Chrome Memory中被标记为 Detached nodes
)
闭包
并不是所有的闭包都可以被称为内存泄漏,只有那些不在预期范围内的才是。
PS:现代浏览器的实现机制是,如果闭包return的变量并没有被引用,那么还是会在下次垃圾回收时清理。
控制台打印信息
一些变量由于始终被console引用,所以会始终在内存中。
使用Chrome Memory分析泄漏原因
操作步骤
注意
使用
无痕模式
调试,避免浏览器插件带来的影响(特别是例如vue-devtools这种插件)。内存回收机制的触发是浏览器自动运行的。但在每次拍摄快照时,浏览器会自动强制执行一次回收机制。也可以点击Memory中的垃圾桶按钮强制回收。
分析结果中
括号里的构造器
是引擎实现相关的,开发者可以先忽略它们。时间轴里留下的构造函数,都是没被垃圾回收机制回收的。(回收的不再显示)
在本地dev测试时,发现切换路由后仍然有很多变量被
__VUE_HMR_RUNTIME__
引用导致没被回收,这是由于dev环境被热更新serve引用,因此想要对项目的性能进行准确的分析,应当在build后测试。
Chrome Memory 中确定问题
从截图里可以看到所有 Object
的缓存基本都是由于在 @wry/context:Slot
中引用。
在
yarn.lock
中找到哪个npm
包引用了@wry/context
...
"@apollo/client@^3.4.17":
dependencies:
"@wry/context" "^0.7.0"
...
optimism@^0.16.1:
dependencies:
"@wry/context" "^0.6.0"
...
可以锁定 @apollo/client@^3.4.17
和 optimism@^0.16.1
这两个组件。
在
@apollo/client
的源码中,找到对@wry/context
的引用
// apollo-client/src/cache/inmemory/reactiveVars.ts
import { dep, OptimisticDependencyFunction } from "optimism";
import { Slot } from "@wry/context";
// Contextual Slot that acquires its value when custom read functions are
// called in Policies#readField.
export const cacheSlot = new Slot<ApolloCache<any>>();
const cacheInfoMap = new WeakMap<ApolloCache<any>, {
vars: Set<ReactiveVar<any>>;
dep: OptimisticDependencyFunction<ReactiveVar<any>>;
}>();
export function makeVar<T>(value: T): ReactiveVar<T> {
// ...
const cache = cacheSlot.getValue();
if (cache) {
attach(cache);
getCacheInfo(cache).dep(rv);
}
// ...
}
开发时如何尽量避免内存泄漏
生产环境清除打印
避免一些变量始终被console引用,导致无法被回收的情况。
使用WeakMap
及时清除引用非常重要。但是,你不可能记得那么多,有时候一疏忽就忘了,所以才有那么多内存泄漏。
最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就可以了。
ES6
考虑到了这一点,推出了两种新的数据结构:WeakSet
和 WeakMap
。它们对于键的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用。
需要注意的是:WeakMap的弱引用仅限于键,值的引用仍然是强引用。
下面以 WeakMap 为例,看看它是怎么解决内存泄漏的。
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
上面代码中,先新建一个 Weakmap
实例。然后,将一个 DOM
节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap
里面。这时,WeakMap
里面对element的引用就是弱引用,不会被计入垃圾回收机制。
也就是说,DOM
节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap
保存的这个键值对,也会自动消失。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap
。
Vue相关Issue
- A memory leak occurred when switching between components #6591
- memory leak through nested reactivity #6994
- Detached nodes after unmount component(Memory leaks) #5363
- chromium