文章输出主要来源:拉勾大前端高新训练营(链接)。小哥哥小姐姐请不要嫌弃啰嗦,下面肯定都是干货。
1 JavaScript性能优化
内存管理:
- 内存: 由可读写的单元组成,表示一片可操作的空间
- 管理:人为操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
- 流程:申请--->使用--->释放
2 垃圾回收
JavaScript中的内存管理机制是自动的,因此垃圾回收也是自动进行的。
JavaScript中的垃圾:
- js中定义的对象不再被引用时就是垃圾
- js中定义的对象占用了内存空间,但是却无法从跟上访问到这个对象,它也是垃圾
对于js中产生的垃圾,js会自动释放他们占用的内存空间,这个过程就是垃圾回收。
可达对象: js中从根出发能够访问到的对象就是可达对象
**js中的根:**全局上变量对象
3 GC算法(Gabbage Collection)
3.1 简介
垃圾:
- 程序中不再使用的对象
- 程序中不能再访问到的对象
GC算法:
垃圾回收是一种查找垃圾、释放空间、回收空间的机制,GC算法就是查找和回收时遵循的规则。
常见GC算法:
- 引用计数
- 标记清除
- 标记整理
- 分代回收
3.2 引用计数算法
原理
核心思想:通过给对象空间设置引用数,当引用关系发生改变时,相应地修改引用数,判断引用数是否为0,为0时候对对象空间进行回收。
核心点:
- 引用计数器(判断是否要回收的标志)
- 引用关系改变时会修改引用计数器(增加或减少)
- 回收时机:引用计数器为0立即回收
优点:
- 引用计数算法在发现引用计数器为0的时候回立即进行垃圾回收,可以做到发现垃圾及时回收
- 通过及时回收垃圾,可以最大限度减清内存的被垃圾沾满的情况,减少程序暂停
缺点:
-
无法回收循环引用的对象
function fn() { const obj1 = {}; const obj2 = {}; obj1.name = obj2; obj2.name = obj1; return 'hello world'; } fn(); //运行过后, obj1, obj2都无法从根开始被找到,但是由于互相引用,引用计数器不为0,因此其占用的内存空间无法被有效释放
-
引用计数法由于维护一组引用计数器,需要时刻去监听引用状态的变化,产生的时间开销比较大
3.3 标记清除算法
原理
核心思想:通过两次遍历所有的对象,第一次遍历所有的对象找到活动的对象并进行标记,第二遍遍历所有的对象,清除没有标记的对象,同时将被标记过的对象的标记去掉,以此实现回收相应的空间。回收空间之后,会将回收的空间放入空闲列表中,方便后续的程序申请使用空间。
核心点:两次遍历
- 第一次遍历,标记活动对象
- 第二次遍历,清除非活动的对象,将第一次遍历标记对象标记去除。
**优点:**解决引用计数器中循环引用对象不能回收的问题。
缺点:
- 由于每次遍历回收的对象空间大小可能不连续,将其放入空闲列表中后,不一定刚好满足后续程序申请空间的大小,会造成比较多的碎片空间无法使用,无法最大化使用空闲空间,造成空间浪费。
- 不会立即回收垃圾对象
3.4 标记整理算法
标记整理算法是标记清除算法的增强操作
原理
核心思想:通过两次遍历所有对象,第一次遍历所有对象找到活动的对象并进行标记(与标记清除法第一遍遍历操作相同)。第二次遍历与标记清除法不同的是,需要先进行一次整理,移动对象的位置,使其占用的内存空间连续,然后对非活动对象进行清除。
核心点:找到活动对象后会对活动对象的空间进行整理,将其整理为在连续空间中的对象
**优点:**相比标记清除发,回收的空间是连续的,解决了回收空间碎片化的问题
缺点:
- 无法立即回收垃圾对象
- 整理对象空间也会有时间消耗
4 V8 引擎
4.1 简介
简介:主流的JavaScript执行引擎,可以高效运行JavaScript代码。
优势:
- 采用即时编译,在执行过程中直接编译为可执行的机器码,不需要进行编译为字节码的操作,速度非常块
- V8的内存设有上限,在64位的操作系统,内存上限不超过1.5G,对于32位操作系统,内存不超过800M。内存设置上限的好处:
- V8主要针对浏览器设计的,对于网页应用,现有内存大小已足够使用
- V8内部的垃圾回收机制决定当前设置的内存上限比较合理。当垃圾内存达到1.5G时,V8采用增量标记算法回收内存空间只需50ms,采用非增量标记需要1s。
4.2 V8垃圾回收策略
回收思想:采用分代回收的思想,将内存空间按照一定规则分为新生代与老生代两个类型的内存区,针对不同代采用不同的算法,达到高效的垃圾回收。
V8中常用的GC算法:
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
4.3 V8中回收新生代对象
- V8内部将空间分为两部分,分别为用于存储新生代对象与老生代对象
- 较小的空间用于存储新生代对象,64位机器为
32M
,32位机器为16M
- 新生代对象指的是存活时间较短的对象
- 例:局部作用域对象在运行完相关代码后不再需要使用,就需要进行回收,全局作用域定义的对象可能需要等待程序执行完后再进行释放,相比来说前者存活时间较短,属于新生代对象
新生代对象回收机制
- 新生代回收策略为:复制算法 + 标记整理算法
- 新生代内存区也会被分为两部分等大大的空间(
From
空间和To
空间) From空间
为使用空间,To空间
为空闲空间- 程序运行过程中,会将活动对象存储在
From空间
- 当
From空间
应用到一定程度后,将会触发GC操作 - 此时的GC算法为标记整理法,对
From空间中
的活动对象进行标记并整理到连续的空间,然后将活动对象拷贝至To
空间 - 此时将
From空间
进行完全释放,其中包括活动对象(在To空间
有一份拷贝)与非活动对象;然后将To空间
与From
空间进行交换(From
变为To
,To
变为From
),完成释放
回收细节
- 在将
From空间
活动对象拷贝至To空间
的过程中可能会出现晋升现象 - 晋升就是将新生代对象移动至老生代进行存储
晋升的的条件(两个判断标准)
- 在一轮GC后仍然存活的新生代对象需要晋升至老生代
- 如果在拷贝过程中(将
From空间
活动对象拷贝至To空间
)如果To空间
的使用率超过25%,也需要将活动的对象全都移至老生代- 原因: 在标记整理并释放完原本的
From空间
后,需要将From
与To
空间进行交换,To空间
将会变为From空间
,假如没有任何限制,其空间在变为From
空间后,剩余的空间要是太小的话可能就不足以放入新的活动对象,因此设置限制。
- 原因: 在标记整理并释放完原本的
4.4 V8回收老生代对象
老生代对象说明
- 老生代区域在64为操作系统中上限1.4G,32位操作系统上限700M
- 老生代对象指的是存活时间较长的对象
老生代对象回收机制
- 老生代对象主要采用标记清除、标记整理、增量标记算法进行回收
- 首先使用标记清除完成垃圾空间的回收:虽然会有碎片化问题,但是速度提升比较明显
- 采用标记整理进行空间优化,时机:当将新生代的对象移至老生代,但老生代由于碎片化导致无法存入移动的对象,此时会采用标记整理进行空间优化
- 最后会采用增量标记的方式进行回收效率提升
新生代 vs 老生代 细节处理
- 新生代对象中采用了空间复制算法,以空间换时间,新生代区域占用内存少,且被一分为二,使用空间复制算法可以极大提高效率,同时不会造成很大的空间浪费。
- 老生代区域内存空间较大,采用与新生代同样的方式进行回收会造成比较大的空间浪费,在较大的空间中进行空间复制也会带来更多的时间消耗,因此不适合采用空间复制算法。
4.5 标记增量优化垃圾回收
垃圾回收会阻塞JavaScript代码的执行,无法同时运行。一般在程序执行完毕后,才会进行垃圾的回收。
标记增量会将一整段的垃圾回收操作拆分为多个小部分进行标记,然后进行清除,组合着完成整个垃圾回收,以替代之前先运行程序,再一口气进行垃圾回收的操作。V8中如果不使用增量标记方法,清除1.5G空间仅需1s,将其拆分为多个间隔在v8中是非常合理的,占用的时间也会很少。
5 浏览器性能工具
5.1 Performance工具介绍
performance工具可以在程序运行过程中对内存变化进行实时的监控,有利于开发者根据内存变化去定位问题所在的代码块。
界面如下:
基本的使用方式为点击录制,然后进行页面的访问,之后点击停止,就可以查看到相应的内存变化的记录以及一些图表,以此进行性能分析。
如下图示:
5.2 内存问题的外在表现
- 在网络正常的情况下,页面经常出现延迟或暂停的状态,可能会跟内存频繁进行GC有关,可以通过performance工具进一步查看内存使用情况,确定是否与内存有关
- 页面持续性出现糟糕的性能,在网络正常的情况下,可能会是由于内存膨胀(当前页面为了达到最优的效果而去申请内存空间,且超过了设备本身能够提供的内存大小)导致的
- 页面性能随着时间的延长变得越来越差,可能会因为存在内存泄漏的问题
5.3 监控内存的几种方式
界定内存问题的标准:
- **内存泄漏:**内存使用持续升高
- **内存膨胀:**在多数设备上都可能存在性能问题,需在多种常用设备进行测试,界定是内存原因还是设备原因
- **频繁垃圾回收:**通过内存变化图进行分析
监控明内存的几种方式:
- 浏览器任务管理器
- Timeline时序图记录
- 利用堆快照查找分离DOM(分离DOM也是一种内存泄漏)
- 借助工具获取内存占用的走势图,对各时间段内存占用进行分析以判断是否存在频繁的垃圾回收
5.4 任务管理器监控内存
基本使用:在chrome中,通过点击更多工具>任务管理器
可以打开浏览器任务管理器
在任务管理器中,右键点击红色框一行,可以添加不同的列,选中JavaScript使用内存,将其添加进监控面板
使用分析
内存占用空间
表示的是DOM节点所占用的内存空间,如果数值不断增大,则说明页面中在持续创建新的DOMJavaScript使用的内存
表示的是js的堆,在()
中的值表示js中可达对象所占用的内存,如果该值持续增加,则表示js在不断创建新对象或已有对象大小一直在增长,程序可能会存在一些问题。
5.4 Timeline记录内存
通过performance中的时序图可以对内存的使用进行记录,通过内存占用的走势图可以对程序性能做一定分析,在出现问题的时间节点还可以定位到相应的页面操作,从而定位问题产生的范围。
5.5 堆快照查找分离DOM
工作原理: 找到js堆,对其进行照片留存,方便开发者对其分析
分离DOM:
- 浏览器界面中元素都是存活在DOM树上的节点,DOM元素的状态不仅有存活的节点,还有垃圾对象的DOM节点以及分离状态的DOM节点
- 垃圾对象:DOM元素从dom树上脱离,且js代码中没有地方去引用它
- 分离状态的DOM:DOM元素从dom树上脱离,但是在js代码中仍然有地方在引用它,在内存中仍然占用着内存空间。
拍摄堆快照:
在chrome开发者工具中Memory
选项下可以进行快照的拍摄
通过点击快照,搜索detached
可以搜索到分离状态的dom,分离状态的DOM不会再浏览器中展示,但是却会占用空间,也是一种内存的泄漏,通过对快照查找分离DOM,并确定其位置,可以对相关代码进行合适的优化。
5.6 判断是否存在频繁GC
确定频繁GC的原因:
- GC工作时会阻塞程序的运行
- 频繁且过长的GC会导致应用程序进入假死状态
- 使用户在使用过程中感知到明显的卡顿现象
确定方法:
- 在timeline中内存走势图如果频繁上升下降,可能存在频繁的GC
- 在浏览器任务管理器中,JavaScript内存实际占用空间频繁增加和减小,也可能存在频繁GC
6 代码层面优化
6.1 jsbench
工具介绍
对于测试JavaScript性能,本质上市通过采集大量执行样本进行数学统计和分析。
jsbench
:jsbench.me/是一个基于benchmark.js的工具,可以帮助我们进行上述的统计和分析。
6.2 慎用全局变量
原因
- 查找消耗大: 全局变量定义在全局执行上下文,在所有作用域的顶端,当程序中局部访问到同名变量时,如果局部作用域未定义该变量,则会向上查找,查找消耗大,会降低程序的执行效率。
- **长时间占用内存,在程序结束后才能被GC:**全局执行上下文一直存在上下文执行栈中,直到程序退出才会消失,在程序运行中一直存活因此只有程序结束后才能被GC
- **同名变量遮蔽或污染全局:**在局部作用域中定义同名变量会遮蔽或污染全局
以上分析来看,在使用局部作用域变量的性能会比使用全局变量的方式高。在jsbench
中测试也是同样结果。
6.3 缓存全局变量
在不得不使用全局变量时候,可以通过缓存全局变量进行优化,提高性能。
6.4 通过原型对象添加附加方法
function F1() {
this.foo = function() {
console.log('hello')
}
}
function F2() {
}
F2.prototype.foo = function() {
console.log('hello')
}
const f1 = new F1()
const f2 = new F2()
以上两种方式给对象添加公共的方法,F1通过构造函数直接给生成的每个对象新增一个方法,F2通过prototype
添加了公共方法。
对比两种方法,通过原型链添加的方式只需添加一次,因此效率更高。
6.5 闭包陷阱
闭包
js中闭包的产生是由于其函数作用域,外层函数定义的变量在内层函数可以使用,在执行完外层函数后,内层函数仍可访问外层函数中定义的变量。
这也就意味着之前定义的变量在内存中保存着并未释放,因此执行内层函数才能够正常访问。
这就可能带来如果不能即时释放内存,就会产生内存泄漏的问题,这就是闭包陷阱。
function out() {
const name = 'tom'; // 外层函数定义name
return function() {
console.log('hello ', name); //内层函数使用name
}
}
const inner = out();
// 执行内层函数仍可访问到外层定义的name = tom
inner(); // hello tom
inner(); // hello tom
inner(); // hello tom
inner(); // hello tom
闭包陷阱与解决方案
// 闭包陷阱
function foo() {
const el = document.getElementById('btn');
el.onclick = function() { // onclick事件触发的函数作用域在foo内部
console.log(el.id);
}
}
foo() // foo执行后,该函数仍未得到释放
以上代码中,el
保存了对id为btn
的dom元素的引用,并对其添加了onclick事件触发时执行的函数。
但是存在一点问题是,以上使用方式中el
变量无法得到释放,即时id为btn
的元素被移除,但foo中定义的el
变量仍在,其指向的内存空间中的内容仍在,因此会产生内存泄漏。如果此类问题很多,则会影响到性能。
解决方法:
// 解决方法
function foo() {
const el = document.getElementById('btn');
el.onclick = function() { // onclick事件触发的函数作用域在foo内部
console.log(el.id);
}
el = null; // 释放el: 此时btn元素上的onclick事件还在,因为el其实只是个引用变量,本质是个指针,它指向了dom元素所在的空间,释放掉这个指针并不会对对象的内容有影响。
}
foo() // 执行后仍然会给id为`btn`的元素绑定onclick事件,并且及时释放掉了el,在dom元素被移除后,相应的内存空间引用计数为0,会得到有效释放。
对于闭包陷阱,解决它的方法就是想办法及时释放掉闭包中定义的变量。如果可以避开闭包,尽量不使用闭包。
6.6 避免属性方法访问属性
JavaScript的面向对象与Java等不同,其所有属性均可直接访问,不必设置专门的getter方法,通过getter方法去访问对象属性增加了一层定义反而会降低效率。
// 直接访问
const obj = {
name: 'tom'
}
console.log(obj.name);
// 通过属性方法访问
const obj1 = {
name: 'tom',
getName() {
return this.name;
}
}
console.log(obj1.getName());
这里的分析但从执行效率上来讲,在没有必要使用getter方式访问的时候,尽可能直接访问。
6.7 for循环优化
使用for循环时可以通过缓存length
优化for循环。
const arr = [1,2,3,4,5];
for(let i = 0; i < arr.length; i++){
console.log(arr[i])
}
// 优化,减少重复获取arr.length属性
for(let i = 0, let len = arr.length; i < len; i++){
console.log(arr[i]);
}
for in, for, forEach中访问元素
最高效的方式为forEach:在需要访问元素时,尽可能使用forEach,for in
针对对象设置的循环机制,尽量避免数组遍历时使用
6.8 dom节点操作优化
减少dom操作次数,尽可能合并多次dom操作到一次中
由于浏览器dom操作通常伴随着回流与重绘
回流: 浏览器dom tree由于元素规模尺寸、布局、隐藏显示等发生改变后会导致回流,重建render tree。之后导致重绘,重新进行渲染。回流必然导致重绘
重绘: 对展示页面进行重新绘制,重绘可以单独存在,例如更改元素背景色等仅改变页面样式的操作,会导致重绘,但并不会导致回流。
操作dom的消耗会比执行js代码消耗高很多,因此需要尽可能避免多次重复操作dom。例如通过循环产生的dom节点,可以在循环之后进行统一的一次dom操作。
// 未优化,多次执行dom操作
const container = document.createElement('div');
document.body.appendChild(container);
for (let i = 0 ; i < 10; i++) {
const p = document.createElement('p');
p.innerHTML = i;
container.appendChild(p)
}
// 优化:统一执行一次dom操作
const container = document.createElement('div');
for (let i = 0 ; i < 10; i++) {
const p = document.createElement('p');
p.innerHTML = i;
container.appendChild(p)
}
document.body.appendChild(container);
使用cloneNode
使用cloneNode
方法基于已有模板创建dom节点比document.createElemnt
创建节点效率高。如果可以
6.9 字面量替换new操作
对于一些方便用字面量创建的对象,使用字面量的方式效率会比使用new更高.
例:
const arr = [1,2,3,4,5];
const arr1 = new Array(5);
for(let i = 0, len = arr1.length; i < len; i++) {
arr1[i] = i + 1;
}
// arr创建性能高于arr1