共计 5567 个字符,预计需要花费 14 分钟才能阅读完成。
手上负责的 vue 项目最近出现一个这样的问题,用户用着用着就出现:”喔唷,崩溃啦!“的提示。
做了以下性能优化尝试:
- 主动销毁对象及其子对象
- 主动取消监听 listener
- 本地搜索减少组件 DOM 渲染
主动销毁对象及其子对象
vue-cropper.js,组件实例不会主动销毁,需要主动调用 destroy 方法销毁。
createjs/easeljs,组件实例需要手动销毁 canvas 画布,maker.stage.canvas = null;maker.stage.removeAllChildren();
主动取消监听 listener
createjs/easeljs,maker.stage._eventListeners = null;maker.stage.removeAllEventListeners();
。
本地搜索减少组件 DOM 渲染
iview 的 select 组件,当数据量过大时,DOM 渲染会占用很大的内存,非常吃性能。因此为其增加了渲染指定个数的功能,例如首次渲染只渲染 20 个,之后的搜索从已经加载好的数据中搜索并渲染。
有一定的收效,但是在仍然存在性能问题,切换菜单的过程中,Memory 中的 Javascript VM instance 以 100MB/ 次的速度增加,而且还是在没有数据的情况下。
因此,迫切的需要一次深度的性能优化,以解决当前项目遇到的问题。
解决完这个问题我将增强技能:
- Chrome DevTools 的 Memory,Performance 工具的应用
- vue 相关,javascript 相关,DOM 相关的未知内存泄漏知识点
我将记录以下深度分析内存泄露的相关内容:
- 内存泄露分析 Snapshot 相关知识点
- 内存泄露分析 Snapshot 的疑惑和实践
- Chrome DevTools Elements 的 Event Listeners 分析内存泄露
内存泄露分析 Snapshot 相关知识点
JS heap size
window.performance.memory 对象的属性。
jsHeapSizeLimit: 2197815296
totalJSHeapSize: 12068848
usedJSHeapSize: 10730032
totalJSHeapSize 和 usedJSHeapSize 的区别是什么?
usedJsHeapSize 是内存总数:指的是 JS 对象占用的内存,包括 V8 内部对象 。
totalJsHeapSize 是当前内存总数:指的是 JS 堆的占用的内存,包括任意 js 对象的 空闲内存。
通过以下代码,可以观察当前 document 的 usedJSHeapSize 占用状况,从而分析是否存在内存泄露性能问题。
setInterval(()=>{console.log(performance.memory);
},2000)
通过观察可以发现,js 占用内存(不包括空闲内存)在一直升高,停留一段时间以后也 GC 不到页面初始化的的大小。
因此可以得出结论,存在内存泄露。
也可以在 Chrome 的任务管理器中,开启 JavaScript 使用的内存 的监控。但是这样会开启看到所有 tab 甚至是插件的内存占用信息,不如 code 的方式直观和 geek。
Heap snapshot
堆快照。其实就是当前页面的 js 对象及其相关的 DOM 节点的内存分布情况。
- 内存未泄露堆快照
- 内存泄露堆快照
可以在内存泄露前生成一份堆快照,再在内存泄露后生成一份堆快照。通过对比的方式,找出两份堆快照存在的内存泄露点。最好是在一次操作后分析,以便分析出问题。
Shallow Size
Shallow Size 是对象本身 hold 的内存。
js 会为对象自身开辟一些空间用来存储数据。js 中 string 和 array 会有明显的 shallow size,不过它们主要在渲染内存中存储,在 js heap 上仅仅暴露一个包裹对象。
渲染内存指的是监测页面的所有内存:
- 原生内存(native memory)
- 页面的 js 堆内存(js Heap memory)
- 页面开启的所有 worker 的 js 堆内存(JS heap memory of all dedicated workers)
参考资料:即使是一个小对象,都可能间接的 hold 了庞大的内存。从而导致自动 GC 程序不能处理掉这些被间接 hold 的内存。
Retained Size
这是删除了对象及其依赖对象后,可以释放的内存大小,这些依赖从 GC root 是无法访问到的。
官方解释很拗口,简单理解其实就是 对象及其依赖对象的内存大小。
Comparison 中的分析字段
# New
新创建的对象个数。
# Deleted
删除的对象个数。
# Delta
发生变化的全部对象的个数。净增对象个数。
Alloc.Size
已经分配的使用中的内存空间。
Freed Size
新对象释放出的内存空间。
Size Delta
发生变化的释放内存的全部空间。净增内存空间。
Heap Snapshot 中的 Constructor
- (closure) 通过函数闭包对一组对象的引用计数
- (array、string、number、regexp) 不同对象类型的列表,Array,String,Number,RexExp 的属性
- (已编译代码) 与已编译代码相关的任何内容。
- HTMLDivElement、HTMLAnchorElement、DocumentFragment DOM 对象。
- Dep、Observer、VNode、Watcher、VueComponent 这些是 vue 特有的对象。
一个构造函数的属性
- code :: (CompileLazy builtin) V8 的 builtins
- context :: system/NativeContext V8 的 heap/factory.cc
- feedback_cell::system V8 的 heap/factory.cc
- map::system/Map V8 的 heap/factory.cc
-
shared [V8 的 heap/factory.cc]指的是 SharedFunctionInfos 这是一个介于函数和已编译代码的对象,SFI 没有上下文。
- function_data 函数数据
- name_or_scope_info 函数名称和作用域信息
- script_or_debug_info 脚本或者 debug 信息
Shallow size、Retained size、Freed size、Delta size 的 size 是以什么为单位?
所有的 size 都是以字节为单位的。
Note: Both the Shallow and Retained size columns represent data in bytes.
内存泄露分析 Snapshot 的疑惑和实践
为什么一次菜单切换会导致 6MB 的内存泄露?
素材列表 -> 产品列表 -> 素材列表 ,增加了 6MB 的内存占用。
经过对比发现,主要增大的是 Object 的 Retained Size,从 26913 个(37%)增大到 32933 个(49%),增大了 12%。
刚好 VueComponent 也从 377 个(10%)增大到 600 个(22%),也是从增大了 12%。
所以初步断定,是由于 VueComponent 没有 GC 导致的。
第一组疑问(理论):
- 是有对象没有被销毁吗?
- 是对象销毁了但是由于其他对象依赖它,导致销毁失败吗?
- 是对象销毁了但是由于其他对象依赖它的子对象,导致销毁失败吗?
以上信息是在 Summary 中展示的,那么如何对比两次快照呢?
Chrome DevTools 提供了一个非常便利的功能,Comparison,切换到想要对比的 Snapshot,即可得到 2 次内存占用的 diff。
经过第二次和第一次的对比,我们得到这张对比分析图。
第二组疑问(实践):
组件作为实例的组件,不会跟随父组件自动销毁吗?
是不是通用组件的问题?一个通用组件在多处引用,导致页面销毁后,当前实例的组件没有彻底销毁?
#Delta 值最高的 (closure) 是主要的原因吗?
在 (closure) 的末尾,我们找到很熟悉的通用组件面孔,以此为出发点去做分析。
分析 ./src/components/uploadToOss
组件
shared 是很可疑的,点开以后是下图的场景。
组件在这里出现,说明这个 模块 / 组件闭包内部变量使用完后没有置为 null。
vue 并不会监测到组件 / 模块不再使用,所以我们需要在 vue 的 destroyed 或者 beforeDestroy 生命周期中做主动销毁。
<script>
import ALIOSS from '@/components/uploadToOss';
let commonOSS = new ALIOSS();
export default {beforeDestroy() {commonOSS = null; // 这是新增的代码,销毁创建的上传 OSS 组件实例,释放闭包空间}
}
</script>
一定要注意,vue 是监测不到我们不用某些模块的,只有绑定在 vue 实例上的实例才会与组件一起销毁,没有绑定的一定要主动销毁。
置为 null 前
置为 null 后
我们成功释放了 112byte,也就是 0.112Kb 的内存!
- 是有对象没有被销毁吗?是的,引入的模块没有被销毁。
- 是对象销毁了但是由于其他对象依赖它,导致销毁失败吗?不是,我们暴露的一般是一个 class,新建的实例有自己的上下文,不存在单文件组件间互相引用,因此是独立的。
- 是对象销毁了但是由于其他对象依赖它的子对象,导致销毁失败吗?对象销毁后其子对象也会自动销毁。
- 组件作为实例的组件,不会跟随父组件自动销毁吗?会销毁的。每次引入都是独立的。
- 是不是通用组件的问题?一个通用组件在多处引用,导致页面销毁后,当前实例的组件没有彻底销毁?不是。但不是由于多初引入导致的,而是由于没有主动将组件创建的闭包变量置 null 导致的。
这次分析给了我们一个启示呢?在利用 class Filter 去搜索 constructor,观察 delta size 是否为负数,freed size 是否不为 0,这样就可以判断出模块有没有彻底销毁。
费了半天劲,最后只优化了 0.012Kb,这不和没优化没差吗?
试着从 VueComponent 的对比找找原因:在产品列表快照,我们发现了残留的未被销毁的素材列表的 Table 组件。
所以几乎可以确定的是,切换到素材列表页面的 Table 组件,没有被完全销毁,在产品列表中依然可以找到它的身影。
所以,是 iView 的 Table 组件存在内存泄露?还是 vue 本身存在内存泄露?
再经过对比 element-ui 和 iView,发现 iView 确实是存在内存泄漏的,内存占用一直降不下来,而 element-ui 过一会儿就会降到正常值。所以不是 Vue 的原因。
和老大讨论了一下,之后可能会替换成其他的 UI 框架。
目前的方案是监听 window.performance.memory 对象,一段时间内持续大于某个阀值时,会提醒用户主动刷新页面,从而释放出泄露掉的内存。
关于 iView 内存泄露的讨论:
- https://github.com/iview/ivie…
- https://github.com/iview/ivie…
- https://www.v2ex.com/t/587573…
我的验证方式:
- iView 官网几次切换后停留到同一个页面,element-ui 官网切换 观察同一个页面的内存占用
- 本地项目几次切换后停留到同一个页面,对比 VueComponent 个数,并找出其他页面的组件
就拿这个来说,我做了如下的切换 foo->bar->foo->baz->foo 后,获取到这个快照对比。
从图上可以看出,VueComponent 新建了 612 个,删除了 9 个,净增 603 个,分配了 17.296Kb 的内存,释放了 0.504Kb 的内存(看到这个释放程度我真的佛了),净增 16.792Kb 的内存。造成了 16.792Kb 的内存泄露。
可能你觉得 16.792Kb 不算什么,因为它在我的这次分析里,内存泄露情况只排第 19,排名第一第二的分别泄露了 598Kb,506Kb。
Chrome DevTools Elements 的 Event Listeners 分析内存泄露
vue 中的全局事件销毁,避免 listener 内存泄露。
DOM0 级事件销毁
window.onbeforeunload = () => {};
window.onbeforeunload = null; // 销毁,可以在 vue 的 destroyed 生命周期(最好在这个,因为无需在 beforeDestroy 引用 vue 实例)或 beforeDestroy。
DOM2 级事件销毁
this.foo= (e) => {}
window.addEventListener('resize', this.foo);
window.removeEventListener('resize', this.foo);// 销毁,可以在 vue 的 beforeDestroy 生命周期(引用 vue 实例最好在这个周期销毁)或 destroyed。
全局事件销毁前(内存释放前):
全局事件销毁后(内存释放后):
通过观察可以发现,一次菜单切换,减少了一个冗余的全局事件监听器,性能有些许提升。
总结与思考
经过一系列分析我们发现,可以通过以下几种方式分析内存泄露的问题并修复。
- 监听在 window 的事件没有解绑
- 绑在 EventBus 的事件没有解绑
- 第三方库创建的实例没有调用销毁函数
- 自定义组件 / 模块闭包内部变量未被销毁
前端同学在选型前端 UI 框架时,不妨先测试测试是否存在内存泄露。
斯世浊清,全赖吾辈激扬!
参考资料:
- https://juejin.im/post/5b3195…
- https://webplatform.github.io…
- https://developers.google.com…
- https://www.bitdegree.org/lea…
- https://developers.google.com…