JavaScript优化
内存管理
- 高级语言自带垃圾回收机制
- 如果不注意内存管理,可能会导致内存泄漏问题。
- 内存管理:开发者主动申请空间、使用空间和释放空间。JavaScript中并未提供相应的API,由执行引擎根据语言来执行内存管理操作。
- 申请空间
let obj = {};
- 使用空间
obj.name = 'foo';
- 释放空间
obj = null;
- 申请空间
垃圾回收与常见GC算法
垃圾回收程序执行时,会阻塞JavaScript的执行。
两种对垃圾的判断:
- 对象不再被引用时,称为垃圾
- 对象不能从根上访问时,称为垃圾
可达对象Reachable
- 定义:从根开始,可以访问到的对象就是可达对象(从引用与作用域链上可访问到)
- 在Javascript里,根是全局对象(摘自MDN的描述)
let obj = { name: 'xm'}; // obj可达对象
let ali = obj; // obj被引用
obj = null; // obj空间被释放,但ali还引用着
function objGroup(obj1, obj2) {
obj1.next = obj2;
obj2.prev = obj1;
return {
o1: obj1,
o2: obj2
}
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'});
// 通过下面的两个delete操作,使得obj1对象不具备可达性,也没有了引用,就会被当做垃圾回收
delete obj.o1;
delete obj.o2.prev;
GC算法
- GC :垃圾回收机制
- GC可以找到内存中的垃圾,释放和回收空间
- GC里的垃圾
- 程序中不再需要使用的对象
- 程序中不能再访问到的对象
- GC算法就是GC查找回收垃圾时遵循的规则
常见GC算法
引用计数:使用判断引用的方式
- 核心思想:为对象设置引用数,判断引用数是否为0,为0则回收。
- 引用计数器
- 引用关系改变时,引用数值会被修改。
优点
- 发现垃圾立即回收
- 最大程度减少程序暂停,通过及时释放空间
缺点
- 无法回收循环引用对象
- 时间开销大(要时刻监控对象的引用计数)
标记清除:使用可达对象的方式
核心思想:标记 + 清除,两个阶段
- 遍历所有对象,标记活动对象
- 遍历所有对象,清除标记,便于垃圾回收
回收的空间会放在空闲链表中,方便程序申请内存空间。
优点
- 可以实现对循环引用对象的垃圾回收
缺点
- 不会立即回收垃圾
- 空间碎片化,垃圾对象在内存地址上的不连续导致的。
标记整理:标记清除的增强
清除阶段不同:先执行整理,移动对象的位置,让空间可以连续,再清除标记。
分代回收
老生代对象和新生代对象分别采用不同的空间存储与GC回收算法,见V8引擎。
V8引擎
- 高效执行JavaScript的引擎
- 采用即时编译,源码编译为字节码,而不是机器码。字节码是机器码的抽象描述,比机器码更节省空间
- 内存设限:web应用足够使用,且如果太大的话,回收程序执行时阻塞时间太长。
- 64位操作系统时,不超过1.5G
- 32位操作系统时,不超过800M
V8引擎的垃圾回收
回收主要指的是引用类型,使用分代回收策略
- 内存分为新生代、老生代
- 针对不同对象采用不同算法
V8常用的GC算法有
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
V8的内存回收
- 内存空间一分为二,左侧小空间为新生代区,右侧大空间为老生代区
- 小空间存储新生代对象(32M | 16M)
- 新生代对象指的是存活时间较短的对象(局部作用域的变量)
新生代对象回收
- 复制算法 + 标记整理
- 新生代内存区分为两个等大小空间,使用空间为From,空闲空间为To,申请内存时使用From空间
- 活动对象存储于From空间
- 标记整理后将活动对象拷贝到To空间,From空间与To空间进行了交换
- 释放From空间,这样就变成了新的To空间
晋升
新生代对象移动到老生代,两个判断标准
- 一轮GC后还存活的新生代对象
- To空间的使用率超过25%,则将To空间的活动对象放到老生代
老生代对象回收
- 老生代对象存放在右侧老生代区域
- 64位系统1.4G,32位系统700M
- 老生代对象是指存活时间较长的对象,如全局下的对象,闭包变量
- 主要采用标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间回收
- 采用标记整理算法进行空间优化(当新生代晋升到老生代时,如果空间碎片化导致空间不足是,会执行标记整理)
- 采用增量标记进行效率优化
增量标记
垃圾回收与JavaScript程序交替执行,避免阻塞JavaScript程序。
标记操作与程序执行交替执行,直到标记完成,然后统一进行清除操作,再进行程序的执行。
Performance工具
意义
对内存使用情况进行监控
使用过程
- 输入目标网址(但不要打开)
- 进入开发者工具,选择性能
- 开启录制功能,访问该网址
- 执行用户行为,一段时间后停止录制
- 分析界面中记录的内存信息(需要勾选内存选项)
出现内存问题的体现(在网络环境正常)
- 页面出现延迟加载或经常性暂停,说明GC频繁进行垃圾回收
- 页面持续性出现糟糕性能,说明出现内存膨胀
- 页面的性能随时间越来越差,说明有内存泄漏
监控内存的几种方式
界定内存问题的标准
- 内存泄漏:内存使用持续升高
- 内存膨胀:在多数设备上都存在性能问题
- 频繁垃圾回收:通过内存变化图进行分析
监控内存的方式 - 浏览器任务管理器
- TimeLine时序图记录
- 堆快照查找分离DOM
- 判断是否存在频繁的垃圾回收
任务管理器监控内存
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>任务管理器监控内存变化</title>
</head>
<body>
<button id="btn">add</button>
<script>
const oBtn = document.querySelector('#btn');
oBtn.onclick = function() {
// 通过点击事件增加JavaScript使用的内存;
let arrList = new Array(10000000);
}
</script>
</body>
</html>
关注内存一栏与JavaScript内存一栏,这两栏信息。内存一栏包含了DOM节点使用的内存,而JavaScript内存一栏只是显示JavaScript使用的内存空间。
TimeLine记录内存
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Timeline记录内存变化</title>
</head>
<body>
<button id="btn">add</button>
<script>
const arrList = [];
// 模拟大内存消耗情况;
function test() {
for(let i = 0; i < 100000; i ++) {
document.body.appendChild(document.createElement('p'));
}
// 将数组元素使用x连接成字符串;
arrList.push(new Array(1000000).join('x'));
}
const oBtn = document.querySelector('#btn');
oBtn.addEventListener('click', test);
</script>
</body>
</html>
堆快照查找分离DOM
开发者工具中的内存面板
什么是分离DOM
- 正常DOM节点:界面元素是存活在DOM树上的DOM节点
- 垃圾对象的DOM节点:DOM节点从DOM树剥离,且没有JavaScript引用
- 分离状态detached的DOM节点:DOM节点从DOM树剥离,但有JavaScript引用,会驻留在内存中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>堆快照监控内存</title>
</head>
<body>
<button id="btn">add</button>
<script>
let temEle;
function fn() {
let ul = document.createElement('ul');
for(let i = 0; i < 10; i ++) {
let li = document.createElement('li');
ul.appendChild(li);
}
temEle = ul;
}
const oBtn = document.querySelector('#btn');
// 点击后,temEle中引用的就是detached DOM;
oBtn.addEventListener('click', fn);
</script>
</body>
</html>
GC频繁进行垃圾回收的检测方法
- Timeline中频繁上升下降
- 任务管理器中数据频繁的增加减少
JavaScript代码优化
精准测试JavaScript的性能
- 本质上是采集大量的执行样本进行数学统计和分析
- 使用基于Benchmark.js 的网站来完成
Jsperf使用流程
- 使用GitHub账号登录
- 填写个人信息(非必须)
- 填写详细的测试用例信息(title、slug)
- 填写准备代码(DOM操作常用)
- 填写必要有setup和teardown代码
- 填写具体的测试代码片段
慎用全局变量
- 全局变量定义在全局执行上下文,是所有作用域链的顶端,查找时间长
- 全局变量一直存在于上下文执行栈,直到程序退出
- 局部作用域出现了同名变量时,会遮蔽全局变量
// 使用全局变量
var i, str = '';
for(i = 0; i < 1000; i ++) {
str += i;
}
// 使用局部变量
for(let i = 0; i < 1000; i ++) {
let str = '';
str += i;
}
在jsperf中检查两种方式的性能
缓存全局变量
将使用中无法避免的全局变量缓存到局部。比如在进行DOM查找时,频繁使用document全局变量,就可以让document缓存到局部,使得对document的引用查找起来更快。
通过原型新增方法
通过构造函数挂载在实例上的方法和通过在原型上挂载一样的方法,每一个实例都有一个一样的方法,显然是浪费空间的,并且挂载在原型上的方法,调用起来效率更高。
避开闭包陷阱
闭包使用不当容易造成内存泄漏
下面的代码演示了闭包造成的内存泄漏情况。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>堆快照监控内存</title>
</head>
<body>
<button id="btn">add</button>
<script>
function foo() {
// 用el变量来引用id为btn的DOM元素
// 本来该DOM元素在浏览器页面中就存在引用(可以从root节点找到),我们又使用el引用了该DOM节点
// 当el引用的DOM元素从DOM树中删除时,由于el还存在着该DOM节点的引用,就使得GC无法回收该DOM节点,造成内存泄漏。
// 因为我们的操作会经常从DOM树中删除节点,一旦操作很多,就会有很多被删除的DOM节点无法回收内存空间
var el = document.getElementById('btn');
el.onclick = function() {
console.log(el.id);
}
// 通过el为DOM元素挂载了onclick点击事件处理函数后,将el赋值为null,可以有效阻止闭包造成的内存泄漏
// el = null;
}
// 执行foo()之后,由于id为btn的DOM元素挂载了onclick事件处理函数,函数内部对el.id有引用
// 所以el变量就成了闭包变量,是无法回收的,但实际上el已经没有什么用处了。
foo();
</script>
</body>
</html>
避免属性访问方法的使用
本质上,JavaScript对象的属性都是外部可见,如果使用方法来控制对属性的访问,无疑是增加了一层重定义,没有访问的控制力。
for循环优化
- 对集合的长度用变量进行缓存,不要每轮循环都去获取。
- forEach性能最佳、其次是优化后的for循环,最差的是for in 循环
使用字面量而不是构造函数
DOM节点添加的优化
- 节点的添加操作必定会有回流与重绘
- 使用文档碎片fragment
document.createDocumentFragment();
来一次性添加很多的节点,比一次次添加这些节点效率要高,因为回流与重绘的次数大大减少了
克隆行为优化创建节点操作
当新增节点时,可以先从已存在的节点中克隆(该节点与我们想要新增的节点有很多相同的属性等等),比直接新增节点要有效率。