JavaScript性能优化

内存管理

内存管理流程
  • 申请内存空间
  • 使用内存空间
  • 释放内存空间

垃圾回收与常见GC算法

js中的垃圾
  • js中的内存管理是自动的;
  • 对象不再被引用时是垃圾;
  • 对象不能从根上访问到时是垃圾。
js中的可达对象
  • 可以访问到的对象就是可达对象(引用、作用域链);
  • 可达的标准就是从根出发是否能够被找到;
  • js中的根可以理解为全局变量对象。

当变量对象不可达时,就会被视作垃圾,js引擎会自动找到它并进行垃圾回收。

GC算法介绍

GC是垃圾回收机制的简写,GC可以找到内存中的垃圾、并释放和回收空间。
算法是工作时查找和回收时所遵循的原则。

  • GC中的垃圾是什么
    • 程序中不再需要使用的对象;
    • 程序中不能再访问到的对象。
常见的GC算法
  • 引用计数
    通过引用计数器对对象进行引用计数,引用关系改变时修改引用数字,当引用数字为0时立即进行回收。
    优点
    1.发现垃圾时立即回收;
    2.最大限度减少程序暂停,减少程序卡顿时间。
    缺点
    1.无法回收循环引用的对象;
    2.时间开销大、资源消耗较大。

  • 标记清除
    分为标记阶段和清除阶段:首先遍历所有对象标记活动对象(可达对象),然后再次遍历清除没有标记对象,回收相应的空间,结束后还会清除所有标记方便下次进行GC。
    优点
    相对于引用计数算法,可以回收循环引用的对象。
    缺点
    1.空间碎片化:当前所回收的对象在地址上不连续,不能最大化地使用空间;
    2.不会立即回收垃圾对象。

  • 标记整理
    可以看作是标记清除的增强,标记阶段与标记清除算法一致,清楚阶段会先执行整理,移动对象的位置。
    优点
    减少碎片化空间。
    缺点
    不会立即回收垃圾对象。

  • 分代回收
    将内存分为新生代、老生代,针对不同对象采用不同的算法。详见V8的垃圾回收策略。

V8引擎的垃圾回收

V8是一款主流的js执行引擎,采用即时编译,内存设限(1.对于浏览器来说足够使用;2.如果上限再大那么垃圾回收时间会超过用户感知)。

V8的垃圾回收策略

采用分代回收的思想。

V8中的常用GC算法
  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 增量标记

增量标记
将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记内存中的一部分对象然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。

V8如何回收新生代对象

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

回收过程采用的是复制算法+标记整理:
新生代内存区被等分为两个空间,使用空间为From存储活动对象,空闲空间为To。开始进行GC,会检查From区中的活动对象,标记整理后将活动对象拷贝至To,清空(释放)From区,最后将From和To互换。

细节说明:
拷贝过程中可能出现晋升(将新生代对象移动至老生代),情况为:
1.一轮GC后还存活的新生代需要晋升;
2.To空间的使用率超过25%。

V8如何回收老生代对象

老生代对象指的是存活时间较的对象。老生代对象存储至右侧的老生代区域(1.4G|700M)。

回收过程主要采用标记清除、标记整理和增量标记:
首先使用标记清除完成垃圾空间的回收,当存在新生代对象晋升到老生代区域而由于空间碎片化导致空间不足时则需要采用标记整理进行空间优化,采用增量标记进行效率优化。

新老生代对象垃圾回收对比
  1. 新生代区域垃圾回收使用空间换时间。在新生代内存中,大部分对象的生命周期较短,因此时间效率可观;
  2. 老生代区域垃圾回收不适合复制算法。老生代内存中可能会存储大量对象,如果再将空间一分为二为造成空间的大量浪费。
V8引擎执行流程

预解析的优点

  • 跳过未被使用的代码;
  • 不生成AST,创建无变量引用和声明的scopes;
  • 依据规范抛出特定错误;
  • 解析速度更快。

全量解析

  • 解析被使用的代码;
  • 生成AST;
  • 构建具体scopes信息,变量引用、声明等;
  • 抛出所有语法错误。

Performance工具

使用目的

Performance提供多种监控方式,让开发者可以时刻关注当前内存的变化以确定当前内存空间的使用是否合理。

使用步骤
  1. 打开浏览器输入目标网址;
  2. 进入开发人员工具面板,选择性能;
  3. 开启录制功能,访问具体界面;
  4. 执行用户行为,一段时间后停止录制;
  5. 分析界面中记录的内存信息。
内存问题的体现
  • 页面出现延迟加载或经常性暂停-存在频繁的垃圾回收
  • 页面持续性出现糟糕的性能-内存膨胀
  • 页面的性能随时间延长越来越差-内存泄漏
监控内存的几种方式

界定内存问题的标准:

  • 内存泄漏:内存使用持续升高;
  • 内存膨胀:在多数设备上都存在性能问题;
  • 频繁垃圾回收:通过内存变化图进行分析。

方式:

  • 浏览器任务管理器;
  • Timeline时序图记录;
  • 堆快照查找分离DOM;
  • 判断是否存在频繁的垃圾回收。
堆快照查找分离DOM

DOM存在的几种状态

  • 界面元素存活在DOM树上;
  • 垃圾对象的DOM节点——DOM从DOM树上脱离,js里没有引用;
  • 分离状态的DOM节点——DOM从DOM树上脱离,js里有引用,界面上看不见但是占用内存浪费空间。
判断是否存在频繁GC

为什么需要确定存在频繁GC?

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

确定方式

  • Timeline中频繁的上升下降;
  • 任务管理器中数据频繁的增加减小。

代码优化实例

堆栈处理
  • JS执行环境;
  • 执行环境栈(ECStack, execution context stack);
  • 执行上下文;
  • VO(G),全局变量对象。
  1. 基本数据类型是按值进行操作;
  2. 基本数据类型的值存放在栈区;
  3. 无论是栈内存还是后续引用数据类型会使用的堆内存都属于计算机内存;
  4. GO(全局对象)。
引用类型堆栈处理
函数堆栈处理
  1. 创建函数和创建变量类似,函数名此时就可以看作是一个变量名,存放在VO中;
  2. 单独开辟一个堆内存用于存放函数体(字符串形式代码),当前内存地址也会有一个16进制数值地址;
  3. 创建函数的时候,它的作用域[[scope]]就已经确定了(创建函数时所在的执行上下文);
  4. 创建函数之后会将它的内存地址存放在栈区与对应的函数名进行关联。

函数执行,目的就是为了将函数对应的堆内存里的字符串形式代码进行执行。代码在执行的时候肯定需要有一个环境,此时就意味着函数在执行的时候会生成一个新的执行上下文来管理函数体中的代码。

函数执行时做的事情:

  1. 确定作用域链:<当前执行上下文、上级执行上下文>;
  2. 确定this指向;
  3. 初始化arguments对象;
  4. 形参赋值:它相当于是变量声明,然后将声明的变量放置于AO;
  5. 变量提升;
  6. 执行代码。
闭包堆栈处理

函数执行时创建的私有执行上下文当中引用的一个堆被外部的执行上下文中的变量所引用,因此当函数执行完后,该私有执行上下文就不能被释放。

  1. 闭包是一种机制,通过私有上下文来保护当中变量的机制;
  2. 也可以认为当创建的某一个执行上下文不被释放的时候形成了闭包;
  3. 保护、保存数据。
闭包与垃圾回收
  1. 浏览器都自有垃圾回收机制(内存管理,V8为例);
  2. 栈空间、堆空间;
  3. 堆:当前堆内存如果被占用,就能被释放掉,但是如果确认后续不再使用这个内存中的数据,可以自己主动置空,然后浏览器就会对其进行回收;
  4. 栈:当前上下文中是否有内容,被其他上下文的变量所占用,如果有则无法释放(闭包)。
循环添加事件实现
  • 闭包
  • 自定义属性
  • 事件委托
变量局部化

可以提交代码的执行效率(减少了数据访问时需要查找的路径)。

缓存数据

对于需要多次使用的数据进行提前保存,后续进行使用。

  1. 减少声明和语句数(词法 语法);
  2. 缓存数据(作用域链查找变快)。
减少访问层级
防抖与节流

在一些高频率事件触发的场景下我们不希望对应的事件处理函数多次执行。
常见的应用场景

  • 滚动事件
  • 输入的模糊匹配
  • 轮播图切换
  • 点击操作
  • ...
    浏览器默认情况下都会有自己的监听事件间隔(4-6ms),如果检测到多次事件的监听执行,那么就会造成不必要的资源浪费。

前置场景:界面上有一个按钮可以多次点击。
防抖:对于这个高频的操作来说,我们只希望识别一次点击,可以认为是第一次或者最后一次;
节流:对于高频操作,我们可以自己来设置频率,让本来会执行很多次的事件触发,按照我们定义的频率减少触发的次数。

防抖函数实现

// 防抖函数实现
/**
* handle 最终需要执行的事件监听
* wait 事件触发多久后开始执行
* immidiate 控制执行第一次还是最后一次,false执行最后一次
*/
function myDebounce(handle, wait, immidiate) {
    // 参数类型判断及默认值处理
    if(typeof handle !== function) throw new Error('handle must be a function')
    if(typeof wait === 'undefined') wait = 300
    if(typeof wait === 'boolean') {
        immidiate = wait
        wait = 300
    }
    if(typeof immidiate !== 'boolean') immidiate = false

    // 所谓的防抖效果就是有一个“人”管理handle的执行次数
    // 如果我们想要执行最后一次,那就意味着无论我们点击了多少次,前面的N-1次都无用
    let timer = null
    return function proxy() {
        clearTimeout(timer)
        timer = setTimeout(()=>{
            handle()
        }, wait)
    }
}

节流函数实现
在自定义的一段时间内让事件触发

// 节流函数实现
function myThottle(handle, wait) {
    // 参数类型判断及默认值处理
    if(typeof handle !== function) throw new Error('handle must be a function')
    if(typeof wait === 'undefined') wait = 500

    let previous = 0
    let timer = null

    return function proxy(...args) {
        let now = new Date()  // 定义变量记录当前执行的时间点
        let self = this
        let interval = wait - (now - previous)

        // 非高频次操作,执行
        if(interval <= 0) {
            // 处理临界情况
            clearTimeout(timer)
            timer = null
            handle.call(self, ...args)
            previous = new Date()
        } else if(!timer) {
            // 当系统中没有定时器,自定义一个定时器让handle在interval之后执行
            timer = setTimeout(() => {
                clearTimeout(timer)  // 清除系统中的定时器,但是timer中的值还在
                timer = null
                handle.call(self, ...args)
                previous = new Date()
            }, interval);
        }


    }
}
减少判断层级

如果出现if多层嵌套的情况,考虑将其改为使用提前return的操作。

减少循环体活动

减少对象成员及数组项的查找次数。每次循环都要查找item.length,这样做很耗时,由于该值在循环运行过程中从未变过,因此产生了不必要的性能损失,提高整个循环的性能很简单,只查找一次属性,并把值存储到一个局部变量中,然后在控制条件中使用整个变量。

字面量与构造式

尽量用对象字面量的方式来创建对象。
字面量的优势:

  1. 它的代码量更少,更易读;
  2. 对象字面量运行速度更快,因为它们可以在解析的时候被优化。

你可能感兴趣的:(JavaScript性能优化)