JavaScript性能优化

Stella981
• 阅读 496

性能优化是一个很大的概念,性能优化的方向有很多比如底层、框架层面上、页面上等等,本篇文章介绍的是JavaScript语言的优化,了解JavaScript的运行的机制

本片文章主要从如下几个方面讲解:

  • 内存管理

  • 垃圾回收与常见GC算法

  • V8引擎的垃圾回收

  • Performance 工具

  • 代码优化实例

内存管理

内存为什么需要管理呢?当程序执行的时候需要去内存申请一片空间进行使用,如果内存不进行管理释放内存空间,那么内存很容易就会溢出。

  • 内存是由可读写单元组成,表示一片可操作空间

  • 管理:认为的的去操作一片空间的申请、使用与释放

  • 内存管理:开发者主动申请空间、使用空间、释放空间

  • 管理流程:申请-使用-释放

JavaScript中的内存管理

  • 申请内存空间

  • 使用内存空间

  • 释放内存空间

如下代码,JavaScript 中的内存管理

`// 申请空间
let obj = {}

// 使用空间
obj.name = 'foo';

//释放空间
obj = null;
`

垃圾回收

JavaScript中的垃圾回收

  • JavaScript中内存管理是自动的

  • 对象不再被引用时是垃圾

  • 对象不能从根上访问到时是垃圾

JavaScript 中的可达对象:

  • 可以访问到的对象就是可达对象(引用、作用域链)

  • 可达的标准就是从根出发是否能够被找到

  • JavaScript中的根就可以理解为全局变量对象

下面通过代码来看JavaScript中的引用与可达

如下代码,obj→xm,ali→xm,当设置obj=null,ali → xm 引用的概念理解很容易,其实引用就是指向了一个内存空间(堆内存),这个内存空间(堆内存)可以被栈内存中的变量指向也就是栈内存中的变量存储的是指向堆内存的指针地址。

`let obj = {name:'xm'} //全局的对象

let ali = obj;//多了一层的引用

obj = null; //obj到xm的引用就断掉了, 但是ali还在引用xm 所以xm是可达的
`

那么什么是可达对象呢?下面来通过一段代码演示.

`function objGroup(obj1,obj2){
    obj1.next = obj2;
    obj2.prev = obj1;

    return {
        o1:obj1,
        o2:obj2,
    }
}

let obj = objGroup({name:'obj1'},{name:'obj2'});

console.log(obj);
`

上述代码可以用下图来表示,全局找到一个可达的对象obj 然后返回了o1o2o1o2 是互相指向的,下面的都是可达的对象

JavaScript性能优化

那么什么情况下是不可达的呢?看下图将obj指向o1的链条断掉,o2指向o1的链条也断掉,那么我们在看从global根出发就找不到o1 那么o1就是一个不可达的对象,也就是垃圾对象会被JavaScript引擎回收掉。

JavaScript性能优化

那么什么是可达,什么是引用就讲述清楚了。

GC算法

  • GC 就是垃圾回收机制的简写

  • GC可以找到内存中的垃圾、并释放和回收空间

GC里的垃圾是什么

  • 程序中不再需要使用的对象

  • 程序中不能再访问到的对象

什么是GC算法

  • GC是一种机制,垃圾回收器完成具体的工作

  • 工作的内容就是查找垃圾释放空间、回收空间

  • 算法就是工作时查找和回收所遵循的规则

常见的GC算法:

  • 引用计数

  • 标记清除

  • 标记整理

  • 分代回收

引用计数算法

核心思想:设置引用数,判断当前引用数是否为0. 引用计数器;引用关系改变时就会修改引用数字,比如有一个内存空间有一个变量指向它引用计数就会加一,如果这个变量不再指向它了引用计数就会减一,当这个内存空间引用数字为0时立即回收。

如下的代码片段,user1 → user3nameList引用着 此时的引用数不是0 就不会被GC回收掉,在fn()函数如果num1 num2 不被const修饰那么num1 num2 就会挂载到全局上,即使函数fn()执行完毕也不会被回收,如果加上const修饰符只作用于函数内部,那么函数执行完毕就会被回收掉。

`const user1 = { age: 11 }
const user2 = { age: 12 }
const user3 = { age: 13 }

//user1 - user3 都被nameList 此时引用数不是0 就不会被GC回收掉
const nameList = [user1.age, user2.age, user3.age];

function fn() {
    //挂载在全局下 加上const 就会只在fn()其效果 一旦函数调用完毕 num1 num2 的引用计数就为0
    const num1 = 1
    const num2 = 2
}

fn();
`

引用计数算法的优点:

  • 发现垃圾时立即回收

  • 最大限度减少程序暂停(应用程序在执行的过程中会对内存进行消耗,内存是有限制的,当内存将要爆满的时候引用计数就会立即找到引用数0的内存空间立即释放)

引用计数算法的缺点:

  • 无法回收循环引用的对象

如下代码片段:函数执行结束以后,内部所在的空间需要被回收因为全局已经没有引用了,但是在函数的内部空间,obj1.name → obj2;[obj2.name](http://obj2.name) → obj1 ,所以obj1和obj2的引用数并不是为0的,那么引用计数算法就无法回收obj1 和 obj2导致内存空间上的浪费。

`function fn1(){
    const obj1 = {}
    const obj2 = {}
    //但是obj2的一个属性是指向了 obj1的两者之间还存在引用 引用计数并不是为0的
    obj1.name = obj2;
    obj2.name = obj1;

    return '';
}

fn1();
`

  • 时间开销大(引用计数要维护着引用数的变化,时刻监控当前对象的引用数值是否需要修改,如果内存中有非常多的对象需要修改,那么时间开销会大一些)

标记清除算法

  • 核心思想:分标记和清除两个阶段

  • 第一个阶段:遍历所有对象找标记活动对象(活动对象:可达对象)

  • 第二个阶段:遍历所有对象清除没有标记对象,并且抹掉第一个阶段的标记,便于下一次的标记清除正常工作

  • 回收相应的空间

看下图来理解标记清除算法:

我们都知道标记清除算法标记的都是可达对象,可达的标准就是全局作用域Global下查找到的对象就是可达对象。下面来仔细看图,图中global是全局作用域就是根,下面的 A B C D E都是可达对象,而右边的obj1obj2 在局部作用域中并且两个互相引用,不是可达对象无法进行标记就会被清除掉,其实标记清除算法也就解决了上述中的引用计数算法 的无法回收循环引用对象的问题。将回收的空间放在「空闲链表」的地方。

JavaScript性能优化

标记清除算法优点:相对于引用计数算法

  • 可解决循环引用对象的问题

标记清除算法缺点:

标记清除算法的空间回收,地址不连续会导致空间碎片化

如下图所示通过标记清除算法标记了可达对象B,而对象A对象C都是不可达的,就会被回收掉他们的内存空间,但是B的内存空间正好在AC的中间位置 这样就会导致回收的空间地址不连续的,比如对象D空间大小正好是2或者1就会被分配到AC,如果D的空间大小是1.5那么找A的空间就会太大,而找C的空间就会太小,这样会导致内存空间会有很多碎片。

JavaScript性能优化

标记整理算法

  • 标记整理可以看做是标记清除的增强

  • 标记阶段的操作和标记清除一致(遍历所有对象找标记活动对象(活动对象:可达对象))

  • 清除阶段会先执行整理,移动对象位置

看下图,是回收前的内存分布,后很多的活动对象、非活动对象、空闲的空间JavaScript性能优化

标记整理算法会在清除的时候先整理内存空间,移动对象的位置,整理的内存空间如下图

把活动对象进行移动在地址上变成一个连续的,然后再将非活动的对象进行回收。

JavaScript性能优化

回收后的内存空间,如下面的图示

相对于之前的标记清除算法就不会大量的分散的碎小的空间,使得回收后的空间尽量是连续的

JavaScript性能优化

在回顾一下常见的GC算法

  • 引用计数

  • 标记清除

  • 标记整理

引用计数优缺点:

  • 可以及时回收垃圾对象

  • 减少程序卡顿时间

  • 无法回收循环引用的对象

  • 资源消耗较大

标记清除优缺点:

  • 可以回收循环引用的对象

  • 容易产生碎片化空间,浪费空间

  • 不会立即回收垃圾对象(清除的时候程序是停止工作的)

标记整理优缺点:

  • 减少碎片化空间

  • 不会立即回收垃圾对象(清除的时候程序是停止工作的)

V8 垃圾回收策略

什么是V8:

  • V8是一款主流的JavaScript执行引擎

  • V8采用即时编译(一般的JS引擎源代码- 字节码才会执行,而V8会直接翻译成机器码)

  • V8内存设有上限的(64位 ≤ 1.5G;32位 ≤ 800M )

V8垃圾回收策略:

  • 采用分代回收的思想

  • 内存分为新生代、老生代

  • 针对不同对象采用不同算法

如下图示V8的垃圾回收策略

JavaScript性能优化

V8中常用GC算法

  • 分代回收

  • 空间复制

  • 标记清除

  • 标记整理

  • 标记增量

V8如何回收新生代对象

首先我们先看一下V8的内存分配,如下图所示左侧红色区域专门存储新生代存储区,右侧为老生代存储区

  • V8内存空间一分为二

  • 小空间用于存储新生代对象(64位→32M | 32位→16M)

  • 新生代指的是存活时间较短的对象 (什么是存活时间较短的对象:当前的代码内有一个变量a在局部作用域,变量b在全局作用域,a的存活时间是比较短的)

新生代对象回收实现:

  • 回收过程采用复制算法 + 标记整理

  • 新生代内存区分为二个等大小空间From 和 To

  • 使用空间为From,空闲空间为To

  • 活动对象存储于From空间

  • 标记整理后将活动对象拷贝至To空间 From空间的活动对象就会有一个备份

  • From与To交换空间完成释放

拷贝过程中可能出现晋升,晋升就是将新生代对象移动至老生代,如果一轮GC还存活的新生代需要晋升,如果To空间的使用率超过25%将新生代对象移动至老生代

JavaScript性能优化

那么V8如何回收老生代呢?

  • 老生代64位→1.4G , 32位→ 700M

  • 老生代对象就是指存活时间较长的对象(如全局作用域下所存放的变量、闭包的情况下所存储的变量数据)

  • 主要采用:标记清除、标记整理、增量标记算法

  • 首先使用标记清除完成垃圾空间的回收

  • 采用标记整理进行空间优化(当新生代区域内容移动至老生代区域,而且老生代的存储空间不足以存储新生代所移动过来的对象,就会执行标记整理优化空间)

  • 采用增量标记进行效率优化

细节对比:

  • 新生代区域垃圾回收使用空间换时间

  • 老生代区域垃圾回收不适合复制算法

关于增量标记算法如何优化垃圾回收?

如下图示

分会两个部分一个是程序的执行一个是垃圾回收,当执行垃圾回收操作会停止程序的执行,将一整段的垃圾回收操作组合的完成垃圾回收,垃圾回收与程序执行交替执行这样所带来的时间消耗会合理一些,程序执行一会标记一轮,最后标记操作完成操作后就进行垃圾回收操作,当垃圾回收操作完成之后程序继续执行操作。以前的垃圾回收会进行一整段操作,也会使程序停顿很长的一段时间。

JavaScript性能优化

回顾V8垃圾回收

  • V8是一款主流的JavaScript执行yinq

  • V8内存设置上限 主要针对浏览器

  • V8采用基于分代回收思想实现垃圾回收

  • V8内存分为新生代和老生代

  • V8垃圾回收常见的GC算法(新生代:复制算法+标记整理;老生代:标记清除 + 标记整理 + 增量标记)

Performance工具

  • GC的目的是为了实现内存空间的良性循环

  • 良性循环的基石是合理使用

  • 时刻关注才能确定是否合理

  • Performance提供多种监控方式

Performance工具是浏览器提供的一种工具,如下图示

JavaScript性能优化

内存问题的体现配和工具进行定位

  • 页面出现延迟加载或经常新暂停 →  频繁的垃圾回收

  • 页面持续性出现糟糕的性能 → 内存膨胀

  • 页面的性能随时间延长越来越差  → 可能会出现内存泄漏

监控内存的几种方式,界定内存问题的标准

  • 内存泄漏:内存使用持续升高

  • 内存膨胀:在多数设备上都存在性能问题

  • 频繁垃圾回收:通过内存变化图进行分析

  • 浏览器任务管理器可以监控内存

  • Timeline时序图记录监控内存

  • 堆快照查找分离DOM

  • 判断是否存在频繁的垃圾回收

监控内存的方式

  • 使用 Chrome 的任务管理器了解您的页面当前正在使用的内存量。

  • 使用 Timeline 记录可视化一段时间内的内存使用。

  • 使用堆快照确定已分离的 DOM 树(内存泄漏的常见原因)。

  • 使用分配时间线记录了解新内存在 JS 堆中的分配时间。

任务管理器监控内存

首先我们需要写一段代码,来模拟内存变化,触发点击事件的时候 创建一个特别大的数组

`

              任务管理监控内存变化     点击      `

如何打开浏览器的任务管理器呢?如下图所示

JavaScript性能优化

打开任务管理器之后找到我们写的对应的页面的任务,然后显示JavaScript的内存

JavaScript性能优化

点击页面的按钮可以明显的看到内存增大了,但是任务管理器无法定位问题,只能够监控JavaScript脚本内存的变化

JavaScript性能优化

Timeline 记录内存

任务管理器更多的是判断当前的脚本的内存是否存在问题,但是无法精准的定位问题。

首先我们需要通过一段模拟的代码

`

              时间线记录内存变化     点击      `

打开性能面板,进行录制,点击几次按钮之后停止录制,就可以得到如下图的程序的运行信息,注重关注JS Heap 可以看到内存是有增长也有降低这是因为点击了按钮内存立马就会增长,而内存下降的原因是执行了垃圾回收操作内存就会下降,在最上面的信息中还可以看到代码执行的时间,从而分析出程序出现的问题。

JavaScript性能优化

堆快照查找分离DOM

  • 界面元素存活在DOM树上

  • 垃圾对象时的DOM节点(从DOM树上脱离,在JS代码中也没有引用)

  • 分离状态的DOM节点(从DOM树上脱离,在JS代码中存在引用,那么这样是有问题的占用内存,需要找到代码进行优化)

首先写模拟代码 创建的DOM但是没有添加到DOM树上,那么这种情况就是分离DOM

`

              堆快照内存变化     点击      `

运行页面,打开开发者工具选择Memory选项,会看到Heap snapshot这个就是堆快照,先不要点击按钮,先进行一次快照和之前的进行对比,点击Task snapshot生成快照

JavaScript性能优化

生成快照 Snapshot1 没有点击按钮之前的快照,检索deta这个就是查找是否存在分离DOM

JavaScript性能优化

之后点击按钮,然后在点击生成快照,看一下两个快照有什么不同,如下图所示点击按钮之后确实在堆中生成了DOM但是并没有在DOM树上引用,这样其实是占用空间,浪费空间的,解决方案:「在确定不使用的地方直接置为null即可」

JavaScript性能优化

判断是否存在频繁GC

  • GC工作时应用程序是停止的

  • 频繁且过长的GC会导致应用致死

  • 用户使用中感知应用卡顿

确定频繁垃圾回收

  • Timeline中频繁的内存上升下降

  • 任务管理器中数据频繁的增加减小 瞬间增大瞬间减小这样的表象就会频繁垃圾回收

Performance总结

  • Performance 使用流程

  • 内存问题的相关分析方式

  • Performance时序图监控内存变化

  • 任务管理器监控内存变化

  • 堆块照查找分离DOM 可能会存在内存泄漏的现象

代码优化

如何进准测试JavaScript性能

  • 本质上就是采集大量的执行样本进行数学统计和分析

  • 使用基于Benchmark.js完成

Jsperf使用流程 测试JavaScript代码

  • 测试用例信息(title、slug)

  • 准备代码(DOM 操作时经常使用)

  • 填写setup和teardown代码

  • 填写测试代码片段

慎用全局变量

  • 全局变量定义在全局执行上下文,是所有作用域链的顶端

  • 全局执行上下文一直存在于上下文执行栈,直到程序退出

  • 如果某个局部作用域出现了同名变量则会遮蔽或污染全局

`//1
var i,str = '';

for(i = 0; i<1000;i++){
    str += i;
}

//2

for(let i = 0; i<1000;i++){
    let str = '';
    str += i;
}
`

通过Jsperf来测试全局变量代码,测试结果如下,很明显全局变量会导致JavaScript的性能下降,在实际开发中要慎用全局变量

JavaScript性能优化

缓存全局变量

将使用中无法避免的全局变量缓存到局部中。

如下代码示例:

`

              缓存全局变量                         

1111

              

2222

              

33333

               `

jsperf 中进行添加测试测试结果如下,缓存全局变量比不缓存全局变量快一些

JavaScript性能优化

通过原型新增方法

在原型对象上新增实例对象需要的方法。

如下代码示例

`//添加到实例内部
var fn1 = function () {
    this.foo = function () {
        console.log(1111);
    }
}

let f1 = new fn1();

//添加到原型上
var fn2 = function () { }
fn2.prototype.foo = function () {
    console.log(1111);
}

let f2 = new fn2();
`

在jsperf上添加两种方式测试代码,进行性能对比,性能上在原型对象上添加方法性能要更好

JavaScript性能优化

避开闭包陷阱

闭包特点

  • 外部具有指向内部的引用

  • 在”外“部作用域访问”内“部作用域的数据

  • 闭包使用不当很容易出现内存泄露

  • 不要为了闭包而闭包

下面来演示闭包导致的内存泄露的问题

`

              闭包陷阱     add     

`

避免属性访问方法使用

  • JS不需要属性的访问方法,所有属性都是外部可见的

  • 使用属性访问方法只会增加一层重定义,没有访问的控制力

如下测试用例,从性能上避免属性访问方法的使用性能上要更好一些

JavaScript性能优化

for循环优化

如下示例代码:主要进行了两个for循环的对比,第一个for循环每次循环获取length,第二个for循环对length进行了保存。

`

              for 循环优化     add

    add

    add

    add

    add

    add

    add

    add

    add

    add

    

`

测试结果 显然对length进行了一个保存性能要更好一些

JavaScript性能优化

For循环优化:提前对length进行缓存。

采用最有循环方式

比对for  forEach for..in..三种循环的对比

如下对比结果forEach循环是最优的,然后是for循环而for..in..是最差的

JavaScript性能优化

节点添加优化

节点添加操作必然会有回流和重绘

通过文档碎片来提高append和created的操作

`

              优化节点添加     

`

克隆优化节点操作

代码示例如下:

`

              Document     old

    

`

下面来看一下性能对比:

JavaScript性能优化

直接量替换Object操作

如下的测试结果,使用var a = [1,2,3] 的性能要更好一些

JavaScript性能优化

总结

  • JS中的内存空间在变量定义时自动分配,程序员无法指定大小

  • JS中内存的生命周期为:申请内存、使用内存、释放内存三个步骤

  • JS中的内存释放可以由开发者自己来完成 JS平台虽然都存在GC机制,但是由于不同算法的限制,代码书写不当同样会导致内存无法回收,产生泄露

  • 标记清除算法的缺点就是找到垃圾对象空间后直接进行回收,而有可能产生大量碎片化空间

  • 在一个作用域链上,只要通过跟可以有路径查找到的对象都是可达对象

  • 标记清除算法的一个阶段会找到所有的可达对象

  • GC操作的执行会导致应用程序的停止,等到GC工作结束之后应用执行才会继续

本文分享自微信公众号 - FrontMagic(JakePrim)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Souleigh ✨ Souleigh ✨
2年前
前端性能优化 - 雅虎军规
无论是在工作中,还是在面试中,web前端性能的优化都是很重要的,那么我们进行优化需要从哪些方面入手呢?可以遵循雅虎的前端优化35条军规,这样对于优化有一个比较清晰的方向.35条军规1.尽量减少HTTP请求个数——须权衡2.使用CDN(内容分发网络)3.为文件头指定Expires或CacheControl,使内容具有缓存性。4.避免空的
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这