Skip to content

浏览器中的内存泄漏

造成内存泄漏的原因分析

  • 全局变量
  • 分离的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

json
...

"@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.17optimism@^0.16.1 这两个组件。

@apollo/client 的源码中,找到对 @wry/context 的引用

ts
// 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 考虑到了这一点,推出了两种新的数据结构:WeakSetWeakMap。它们对于键的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用。

需要注意的是:WeakMap的弱引用仅限于键,值的引用仍然是强引用。

下面以 WeakMap 为例,看看它是怎么解决内存泄漏的。

ts
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

参考