菜单 学习猿地 - LMONKEY

JavaScript日常代码开发代码层面的性能优化

这里不缺理想 profile image 这里不缺理想 ・1 min read

课程推荐:java开发工程师--学习猿地精品课程

随着软件开发行业的不断发展,性能优化已经是一个不可避免的话题,本质上说任何可以提高运行效率,降低运行开销的行为,都可以看做是一种优化操作,前端开发过程中,性能优化无处不在,请求资源的网络,数据传输方式,开发框架等都可以优化,本文探索的是,JavaScript语言本身的优化。
这里主要从内存管理、垃圾回收与常见的GC算法、V8引擎的垃圾回收、Chrome浏览器的performance工具,代码优化实例进行性能优化相关的内容介绍
JavaScript的内存管理
内存:可读写的单元组成,表示一片可操作的空间
管理:人为的去操作一片空间的申请、使用和释放
内存管理:就是开发者中东申请空间、使用空间、释放空间
管理流程:申请--使用--释放
js中,没有直接操作内存的api,所以
// 申请空间 就是声明一个变量,js执行引擎会自动分配一个相应的空间
let obj = {}

// 使用空间 就是读写操作
obj.name = "lg"

// 释放空间
obj = null

// 按照内存管理的流程,实现了js的内存管理
复制代码
JavaScript的垃圾回收
引用、从根上访问
js中可达对象
可以访问到的对象就是可达对象(可以通过引用,作用域链找到的对象)
可达的标准就是从根出发是否能被找到
js的根就可以理解为全局变量对象,也就是全局执行上下文
js中的引用和可达
function objGroup(obj1, obj2){
obj1.next = obj2
obj2.prev = obj1
return {
o1: obj1,
o2: obj2
}
}
let obj = objGroup({nam: 'obj1'},{name: 'obj2'})
console.log(obj);
// 这里obj obj1 obj2都是可以直接或者通过属性的引用从根上可达的
复制代码
GC算法介绍
GC的定义和作用
GC就是垃圾回收机制的简写
当GC工作的时候,可以找到内存中的垃圾对象,然后对于这些空间进行释放,并且回收分配方便后续代码使用
GC里的垃圾是什么
程序中不在需要使用的对象
从程序需求的角度考虑,某个数据使用完之后,上下文不再需要用到它了,就可以当做垃圾看待
function func(){
name = 'lg'
return ${name} is a coder
}
func()
如例子中的name,执行完不再用到了,应该被当做垃圾进行回收的,至于有没有被回收,现在不做讨论
复制代码
程序中不能再访问到的对象
从当前程序运行过程中,变量能否被引用到的角度考虑,
function func(){
const name = 'lg'
return ${name} is a coder
}
func()
当函数调用完之后,我们在外部空间就不能再访问到name了,当我们找不到他的时候,它可以算作是一种垃圾
复制代码
GC算法是什么
GC是一种机制,它里面的垃圾回收器会完成具体的回收工作
工作内容的本质就是查找垃圾,释放空间、回收空间
算法就是工作时查找和回收所遵循的规则
常见的GC算法
引用计数算法实现原理
核心思想:内部通过一个引用计数器,来维护当前对象的引用数,当判断当前对象的引用数为0的时候,GC就开始工作,将其所在的对象空间进行回收和释放再使用
当某一个对象的引用关系发生改变,引用计数器主动修改这个对象所对应的引用数值,当引用数字为0的时候,GC工作,立即回收
引用计数算法的优缺点
优点
发现垃圾时立即回收(及时回收)
最大限度减少程序暂停(当发现内存即将占满的时候,引用计数器回去立马找到数值为0的,对其进行释放)
缺点
无法回收循环引用的对象(循环引用对象引用数字永远不为0)
资源消耗大(需要维护数值,数值修改需要时间,很多对象的时候,频繁操作就会消耗很多时间,是相对于其他算法而言的)
// 循环引用案例
function fn () {
const obj1 = {}
const obj2 = {}

obj1.name = obj2
obj2.name = obj1

return 'lg'

}
复制代码
标记清除算法的实现原理
核心思想:分标记和清除两个阶段完成
第一个阶段,遍历所有对象,找到所有的活动对象(可达对象),(如果找到,还有子对象,就进行递归查找)进行标记操作
第二个阶段,遍历所有对象,直接把身上没有标记的对象进行清除,有标记的身上的标记抹掉,便于下次能够正常工作
通过两次遍历行为,把当前的垃圾空间进行回收,最终把回收的空间交给相应的空闲列表进行维护,给后续代码使用
标记清除算法的优缺点
优点
相对于引用计数算法,解决了循环引用的对象清除问题
缺点
不会立即回收垃圾对象,并且在清除的时候,当前程序是停止工作的
空间碎片化,不能让空间得到最大化的使用
由于当前回收的垃圾对象在地址上是不连续的,由于这种不连续,造成回收之后,分散在各个角落,后续想要使用,后续代码需求的空间,如果比单个多了或者少了,那么之前释放的那片空间就不适合使用了
标记整理算法的实现原理
标记整理算法可以看成是标记清除的增强操作
第一阶段,与上方的标记清除算法第一阶段完全一致
第二阶段,会先执行一个整理的操作,移动对象的位置,在内存地址上产生连续
使活动对象内存地址连续,非活动对象内存地址连续,相对于标记清除算法来说,在内存中不会大批量的出现分散的小空间,回收到的空间是连续的,后续使用过程中,最大化利用当前内存中释放出来的空间
优点
相对于标记清除算法,减少了碎片化空间
缺点
不会立即回收垃圾对象
标记整理算法会配合着标记清除算法在V8引擎中配合实现频繁的GC操作
认识V8引擎
V8是一款主流的js执行引擎
V8采用即时编译,别的很多的js引擎需要将js代码转换成字节码,然后执行,V8直接将源码转成可以执行的机器码
V8的内存设有上限的 64位1.5G 32位800M 垃圾回收机制和为网页应用而生
V8的垃圾回收策略
回收指的是存储在堆区里面的对象类型的数据
内存设有上限所以
采用分代回收的思想
内存分为两类,新生代存储区、老生代存储区针对不同代采用不同的GC算法
V8常用的GC算法

分代回收 空间复制 标记清除 标记整理 标记增量

V8如何回收新生代对象

V8内存空间一分为二
小空间用于存储新生代对象(64位32M,32位16M)
新生代指的是存活时间较短的对象

新生代对象回收实现
1、回收过程采用复制算法+ 标记整理,把左侧的新生代内存区分为两个相等的空间,使用空间为from,空闲空间为to
2、代码在执行的时候,需要申请空间的时候,会将所有的活动对象分配至from空间,这个过程中to是空闲的
3、当form空间使用到一定程度的时候,就要去触发GC操作,采用标记整理的操作对from空间活动对象进行标记,使用整理操作使位置变得连续,以便后续不会产生碎片化空间
4、将活动对象拷贝至to空间,from释放掉,完成释放,最后进行from和to空间的交换
回收细节说明
拷贝过程中可能出现晋升,晋升就是将新生代对象移动至老生代存储区
判断晋升的标准:

一轮GC之后,还存活的新生代需要晋升
如果在拷贝的过程中,to空间使用率超过25%,需要将这次的活动对象移动至老年存储区进行存放

25%以上的时候,当to使用占比达到一定的界限的时候,当from和to交换的时候,从to变成from空间的时候(变成使用状态时),新的对象存储不进去了
老生代对象
老生代对象存放在右侧的老生代区域,64位大小1.4G,32位大小700M,老生代对象就是指存活时间较长的对象
老生代对象回收实现
主要采用标记清除、标记整理、增量标记算法

首先主要采用标记清除完成垃圾空间的回收
当新生代区域要把内容移动到老生代区域,然后老生代空间不足以存放新生代存储所移过来的对象(其实就是晋升),触发标记整理,回收之前的碎片空间,这样就有更多的空间可以使用
采用增量标记的算法,对当前回收效率进行提升

与新生代垃圾回收的对比

新生代区域的垃圾回收,使用空间换时间,采用的是复制算法,虽然一直有闲置空间,但是原本新生代空间就比较小,分出去的就更小,这样的空间浪费,相对于带来的时间上的提升,是微不足道的

老生代存储的内容比较多,如果使用复制算法的话,需要的时间也很多,空间浪费也严重,所以不适合使用复制算法

增量标记算法就是把标记清除算法的第一步的标记操作碎片化,因为js执行和GC标记是不能同步进行的,所以把时间很长的标记操作,分成多个标记操作,就标记一会儿,程序执行一会儿,标记一会儿,程序执行一会儿,交替操作,让整个流程更顺畅,最大限度把停顿时间拆成更小段,对用户更友好
Performance工具的介绍
GC的工作目的让内存空间在程序运行的时候出现良性循环使用
良性的基础就是对内存空间进行合理的分配
时刻关注才能确定内存空间的使用是否合理
Performance提供多种监控方式
优点:通过Performance能时刻监控内存
使用步骤:

打开浏览器输入目标地址
进入开发人员工具面板,选择Performance
开启录制功能,访问具体的界面
执行用户行为,一段时间后停止录制
分析界面中记录的内存信息

内存问题的体现
页面出现延迟加载或经常性的暂停 --可能是当前GC频繁的垃圾回收操作有关
页面持续性出现糟糕的性能 ------可能内存膨胀
页面的性能随着时间延长越来越差 -----可能内存泄漏
内存膨胀的概念:应用程序本身,要达到某种性能,需要很大的内存空间,然后当前设备硬件不支持,
监控内存的几种方式

内存泄漏:内存使用持续升高,没有下降的节点
内存膨胀:在多数设备商都存在性能问题
频繁垃圾回收:通过内存变化图进行分析

浏览器任务管理器
创建一个标签,添加点击事件,点击创建一个长度很长的数组,模拟内存的消耗,内存(dom节点占据的内存),JavaScript内存(界面中所有可达对象在使用的内存大小)
const Btn = document.getElementById('btn')
Btn.onclick = function (){
let arrlist = new Array(1000000)
}
复制代码
更多的是判断有没有问题,但是不好判断哪里出了问题
Timeline时序图记录蓝色线条
放置一个dom节点,添加点击事件,创建大量dom节点,模拟内存消耗,通过数组,配合其他方法模拟大量内存消耗,
const arrrList =[]
function test () {
for(let i =0;i < 100000; i++){
document.body.appendChild(document.createElement('p'))
arrrList.push(new Array(1000000).join('x'))
}
}
document.getElementById('btn').addEventListener('click',test)
复制代码
浏览器Performance模块 => 录制 => js堆
浏览器堆快照查找分离domdetached
什么是分离dom

界面元素存活在DOM树上
垃圾对象的DOM节点 --- 如果这个dom节点从dom树上脱离,js代码中也没有引用的dom节点
分离状态的DOM节点 --- 如果这个dom节点从dom树上脱离,js代码中还在引用的dom节点

分离dom在界面上是看不见的,但是在内存中是占据内存空间的,这种情况下是一种内存泄漏


// 新建dom,用一个变量引用
var tmpEle

function fn() {
var ul = document.createElement('ul')
for(var i=0;i<10;i++){ var li = document.createElement('li') ul.appendChild(li) } tmpEle =ul // tmpEle = null } document.getElementById('btn').addEventListener('click',fn) 复制代码 控制台=>memory=>快照 detached
判断是否存在频繁的垃圾回收,借助于不同的工具,分析,得出判断
为什么要确定频繁的垃圾回收

GC工作是应用程序是停止的
频繁且过长的GC会导致应用假死
用户使用中感知应用卡顿

如何确定呢
Timeline中频繁的上升下降
任务管理器中的数据频繁增加减小
日常开发中代码优化的介绍(注意点)
快的不一定是最优的,要根据代码的可读性和性能等条件综合考虑代码的编写
如何精准测试JavaScript性能
本质上是采集大量的执行样本进行数学统计和分析
使用基于Becnchmark.js的jsperf.com/ 完成
Jsperf 使用流程
使用Github账号登录
填写个人信息
填写详细的测试用例信息(title,slug)
填写准备代码(DOM操作时经常使用到的,没有就不填
填写必要有setup(准备)与teardown(代码销毁)代码,没有就不填
填写测试代码片段
改用 jsbench 来处理吧 网址:jsbench.me
jsbench的使用方法和jsperf使用方法大致相同
慎用全局变量

全局变量定义在全局执行上下文,是所有作用域链的顶端,(按照层级往上查找的过程,下面局部作用域没有找到变量,最终都会去查找到最顶端的全局作用域,查找耗时,降低效率)
全局上下文一直存在于上下文执行栈,知道当前程序退出才消失,(对于GC工作也是不利的,降低程序运行过程中,对于程序的使用)
如果某个局部作用域出现了同名变量,则会造成全局变量的命名污染,遮蔽当前的全局的数据

// 去jsperf.com比较代码
var i, str = ''
for(i=0;i<1000;i++){
str += i
}

for(let i=0;i<1000;i++){ let str str += i } 使用jsperf去做测试 结果是:块级作用域的性能更好 复制代码 缓存全局变量 将使用中无法避免的全局变量缓存到局部 // 定义一段html代码



11111




22222





3333333



// 定义了两个获取元素的函数
function getBtn() {
    let oBtn1 = document.getElementById("btn1")
    let oBtn3 = document.getElementById("btn3")
    let oBtn5 = document.getElementById("btn5")
    let oBtn7 = document.getElementById("btn7")
    let oBtn9 = document.getElementById("btn9")
}
function getBtn2() {
    let obj = document        // 对于全局变量document 做了局部的缓存
    let oBtn1 = obj.getElementById("btn1")
    let oBtn3 = obj.getElementById("btn3")
    let oBtn5 = obj.getElementById("btn5")
    let oBtn7 = obj.getElementById("btn7")
    let oBtn9 = obj.getElementById("btn9")
}
使用jsperf去做测试    结果是:对于全局变量document 做了局部的缓存性能更好

复制代码
通过原型对象添加附加方法
构造函数内部添加方法和原型对象上添加方法两者之间的性能
var fn1 = function() {
this.foo = function() {
console.log(111111);
}
}
let f1 = new fn1()

var fn2 = function() {}
fn2.prototype.foo = function() {
console.log(111111);
}
let f2 = new fn2()
使用jsperf去做测试 结果是:在原型对象上添加方法性能更好
复制代码
避开闭包陷阱
闭包的特点:

外部具有指向内部的引用
可以在外部作用域访问内部作用域的数据

function foo() {
var name = 'lg'
return function fn (){
console.log(name);
}
}
var a = foo() // a =null 消除引用
a()
复制代码
闭包是一种强大的语法
闭包使用不当容易出现内存泄漏
不要为了闭包而闭包
避免属性访问方法的使用
JS不需要属性的访问方法,所有属性都外部可见的
使用属性访问方法只会增加一层重定义,没有访问的控制力
// 第一种
function Person() {
this.name = 'log'
this.age = 18
this.getAge = function() {
return this.age
}
}
const p1 = new Person()
const a=pi.getAge()
// 第二种
function Person() {
this.name = 'log'
this.age = 18
}
const p2 = new Person()
const pAge2 = p2.age
// 使用jsperf去做测试 从执行速度上来说,直接通过属性访问方法更快
复制代码
For循环优化
{/*

add

add


add


add


add


add


add


add


add


add

*/}

var btns = document.getElementsByClassName('btn')
// 第一种
for (var i = 0; i < btns.length; i++) {

}
// 第二种
for (var i = 0, len = btns.length; i < len; i++) {
// 使用jsperf去做测试 毫无疑问,这边的执行效率要比上面的效率高得多 ,原因
}
复制代码
采用最优的循环方式
采用多种选择遍历时,有多种 for in /for/ foreach
var arrList =new Array(1,2,3,4,5,6,7)
// 第一种
arrList.forEach(function(item){
console.log(item);

})
// 第二种
for(var i = arrList.length;i;i--){
console.log(arrList[i]);
}
// 第三种
for(var i in arrList){
console.log(arrList[i]);
}

// 使用jsperf去做测试 性能foreach最好 for in最差
复制代码
节点添加的优化操作
节点添加操作必然会有回流和重绘
// 第一种
for (i = 0; i < 10; i++) {
var op = document.createElement('p')
op.innerHTML = i
document.body.appendChild(op)
}
// 第二种
const fragEle = document.createDocumentFragment()
for (i = 0; i < 10; i++) {
var op = document.createElement('p')
op.innerHTML = i
fragEle.appendChild(op)
}
document.body.appendChild(fragEle)

// 使用jsperf去做测试 文档片段添加节点优于一般的添加节点
// 但是jsbench测试 多数情况下文档片段添加节点却是要慢的
// 所以理论上和实际现象有时候不一定是完全吻合的
主要是了解一下document.createDocumentFragment() 创建一个新的空白的文档片段
复制代码
克隆优化节点操作

old

// 第一种
for (i = 0; i < 3; i++) {
var op = document.createElement('p')
op.innerHTML = i
document.body.appendChild(op)
}
// 第二种
var oldp = document.getElementById('box1')
for (i = 0; i < 3; i++) {
var op = oldp.cloneNode(false)
op.innerHTML = i
document.body.appendChild(op)
}
// 使用jsperf去做测试 克隆方法是明显优越的
// 但是jsbench测试 多数情况下克隆节点却是要慢的
// 要自己去实践
复制代码
字面量替换 new Object()
堆栈中代码执行流程

栈内存和堆内存的回收机制和时机
浅谈js执行的AO/VO
减少判断层级
// 第一种
function doSomthing(part,chapter) {
const parts =['ES2016','工程化','vue','react','Node']
if(part){
if(parts.includes(part)){
console.log('属于当前课程');
if(chapter){
console.log('您需要提供vip身份');
}
}
}else {
console.log('请确认信息');
}
}
doSomthing('ES2016',6)
// 第二种
function doSomthing(part, chapter) {
const parts = ["ES2016", "工程化", "vue", "react", "Node"];
if (!part) {
console.log("请确认信息");
return;
}
if (!parts.includes(part)) return;
console.log("属于当前课程");
if (chapter) {
console.log("您需要提供vip身份");
}
}
doSomthing("ES2016", 6);
复制代码

每当我们遇见多层嵌套的if...else时,可以考虑一下,是否可以通过这种提前return的方法,减少嵌套层级
当遇见多个else if时,而且else if后面的值是固定的,建议使用switch case或者Map,if...else更适合做一些区间性的判断

减少作用域链查找层级
每当一个函数执行的时候,都会产生一个执行上下文,在一个函数体内,多次调用同一个函数的时候,会创建多个执行上下文,这些执行上下文都是有自己的作用域的,这些作用域之间呢,又可以通过作用域链进行连接
// 作用域链查找层级
// 第一种 慢一点 占用更少的内存空间
var name = 'zce'
function foo() {
name ='zce666' // 这里的name是属于全局的
function baz () {
var age = 38
console.log(age);
console.log(name);
}
baz()
}
foo()
// 第二种 快一点 空间换时间 占更多的内存空间
var name = 'zce'
function foo() {
var name ='zce666' // 这里的name是属于当前作用域的,但是需要新开辟空间存放name的值的
function baz () {
var age = 38
console.log(age);
console.log(name);
}
baz()
}
foo()
复制代码
减少数据的读取次数
提前缓存,减少层级查找

var oBox = document.getElementById('skip')
// 第一种
function hasEle(ele,cls) {
return ele.className === cls
}
// 第二种
function hasEle1(ele,cls) {
var clsname =ele.className //快 消耗空间
return clsname === cls
}

console.log(hasEle(oBox,'skip'));
复制代码
字面量与构造函数声明的测试
// new创建obj
let test = () => {
let obj = new Object(); // 好比调用一个函数,消耗时间
obj.name = "zce";
obj.age = 38;
obj.slogan = "我为前端而活";
return obj;
};
// 字面量声明 执行效率高使用
let test = () => {
let obj = {
name: "zce",
age: 38,
oslogan: "我为前端而活",
};
return obj;
};
console.log(test());
复制代码
new String()调用函数,生成的是一个字符串对象,能直接调用对象原型上的方法
字面量生成的string,(不可以调用原型的方法的,而我们用字面量声明的,可以使用原型对象的方法,原因是在调用方法前,会在内部把字符串转成对象)是直接在堆区中开辟空间,往里放东西
减少循环体中的活动
循环体内的操作尽可能的往外提出来
减少声明及语句数
var oBox = document.getElementById('box')
// 方法一
var test =(ele) => {
let w = ele.offsetWidth
let h = ele.offsetHeight
return w h
}
// 方法二
var test1 = (ele) => {
return ele.offsetWidth
ele.offsetHeight
}
console.log(test(oBox))
// jsbench测试 方法二要快的
复制代码
惰性函数与性能
惰性载入表示函数执行的分支只会在函数第一次掉用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了。
前端开发者进阶之惰性函数定义
惰性函数模式
function foo(){
console.log(this)
}

    var obtn = document.getElementById('btn')
    // 第一种
    function addEvent(obj,type,fn){
        if(obj.addEventListener){
            obj.addEventListener(type,fn,false)
        }else if(obj.attachEvent) {
            obj.attachEvent('on'+type,fn)
        }else {
            obj['on'+type] = fn
        }
    }
// 第二种
    function addEvent(obj,type,fn){ // 惰性函数
        if(obj.addEventListener){
            addEvent = obj.addEventListener(type,fn,false)
        }else if(obj.attachEvent) {
            addEvent = obj.attachEvent('on'+type,fn)
        }else {
            addEvent = obj['on'+type] = fn
        }
        return addEvent
    }

    addEvent(obtn,'click',foo)

    //jsbench  第二种是慢的

复制代码
采用事件委托
事件委托的本质就是利用事件冒泡的机制,把原本应该绑定在子元素上的事件绑定在了父元素身上,让父元素完成了事件的监听,算是委托给了父元素,好处是可以大量减少内存的占用,减少事件的注册。
案例就不用说了把,往上大量文章。
the end!啾咪!

文章来自:https://juejin.im/post/6891630175331549191

评论 (0)