美文网首页
学习笔记(六)——JavaScript 性能优化

学习笔记(六)——JavaScript 性能优化

作者: 彪悍de文艺青年 | 来源:发表于2020-09-01 16:53 被阅读0次

    最近参加拉勾教育大前端高薪训练营,开始对前端知识体系进行一个系统的扩展跟学习,通过每天定期的学习,对一些不常使用的知识点进行了了解和补充,同时对已经使用过的知识点温故而知新
    在此记录学习笔记,并根据学习进度定时跟新

    内存管理

    由开发者主动申请、使用、释放内存空间

    JavaScript中的内存管理是自动的

    • 申请内存空间
      • JavaScript中没有提供内存空间申请的API
      • 当定义一个变量,变量被声明赋值时,系统自动分配相应的内存空间
    • 使用内存空间
      • 对变量进行修改赋值等操作
    • 释放内存空间
      • JavaScript中没有提供内存空间释放的API
      • 手动将变量赋值为null,使内存空间不再被变量所引用,内存将在下次垃圾回收过程中被释放

    垃圾回收

    • 什么是JavaScript中的垃圾?
      • 对象不再被引用时,是垃圾
      • 对象不能从根上被访问到时,是垃圾
    • 可达对象
      • 可以访问到的对象,即为可达对象
      • 可达的标准是从根出发能否被访问到
      • JavaScript中的根可以理解为全局变量对象

    GC算法介绍

    GC是垃圾回收机制的缩写

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

    • GC的垃圾是什么
      • 程序不再使用的对象
      • 程序不能再访问到的对象
    • 常见GC算法
      • 引用计数
      • 标记清除
      • 标记整理
      • 分代回收

    引用计数算法

    核心思想是,设置引用数,判断当前引用数是否为0,为0则进行垃圾回收

    引用关系发生改变时,引用计数器修改引用数

    • 优点
      • 发现垃圾立即回收
      • 最大限度减少程序暂停
    • 缺点
      • 无法回收循环引用的对象(引用计数不为0)
      • 资源开销大

    标记清除算法

    核心思想是,分为标记和清除两个阶段

    先遍历所有对象,找到活动对象(可达可访问)进行标记

    再遍历所有对象,回收未被标记的对象,并清除已被标记的对象的标记

    • 优点
      • 可以回收循环引用对象(不可达对象不会被标记)
    • 缺点
      • 内存空间碎片化(内存地址不连续),浪费空间

    标记整理算法

    标记整理算法是标记清除算法的增强

    标记阶段与标记清除算法相同

    清除阶段会先进行整理操作,移动对象位置,使内存地址连续

    V8引擎

    V8是一款主流JavaScript执行引擎

    采用即时编译

    内存设有上限

    • V8垃圾回收策略
      • 采用分代回收思想,将内存分为新生代、老生代,针对不同对象采用不同算法
    • V8中常用的GC算法
      • 分代回收
      • 空间复制
      • 标记清除
      • 标记整理
      • 增量增量
    • V8内存分配
      • 内存空间分为两个部分
      • 小空间(64位系统32M|32位系统16M)用于存放新生代对象(存活时间较短的对象)
      • 大空间(64位系统1.4G|32位系统700M)用于存放老生代对象(存活时间较长的对象,例如全局对象下存放的对象,以及闭包中存放的对象等)
    • 新生代对象回收
      • 采用空间复制 + 标记整理算法
      • 将新生代内存区分为大小相等的两个空间,使用空间为From,空闲空间为To
      • 触发GC时,使用标记整理算法对From空间进行标记,并将活动对象拷贝到To空间
      • 回收释放From空间,并与To空间进行互换
      • 新生代对象从From空间拷贝到To空间过程中可能出现晋升操作(将新生代对象移动至老生代对象存储区域)
        • 一轮GC后还存活的新生代对象将晋升
        • To空间使用率超过25%时
    • 老生代对象回收
      • 采用标记清除、标记整理、增量标记算法
      • 先使用标记清除完成垃圾空间回收(回收速度快)
      • 当出现晋升时,如果空间不足,会使用标记整理算法进行空间优化
      • 采用增量标记进行效率优化
        • 垃圾回收会阻塞JavaScript程序代码执行
        • 将垃圾回收的对象进行分段,使垃圾回收过程与程序代码执行交替进行,避免需要回收的对象较多时(上限1.5G,回收只需1秒)长时间阻塞程序执行
    • 新生代回收 vs 老生代回收
      • 新生代空间小,使用空间换时间(空间复制算法)
      • 老生代空间大,不适合使用空间复制算法

    Performance工具介绍

    浏览器中查看web程序执行性能的辅助工具,提供多种监控方式

    • 使用方式
      • 打开浏览器,输入目标网址
      • 打开开发人员工具,进入Performance工具选项页面,点击录制
      • 访问目标网址,进行用户操作
      • 停止录制
      • 查看性能监控信息

    监控内存的几种方式

    • 浏览器任务管理器
      • 快捷键Shift + Esc (Chrome) 查看JavaScript内存使用列
    • Timeline时序图记录
      • Performance工具中录制查看
    • 堆快照查找分离DOM
      • 什么是分离DOM?
        • 脱离DOM树,未被代码引用的DOM为垃圾,会被回收,仍被代码引用的DOM,即为分离DOM
      • 使用浏览器开发者选项中的内存工具Memory,获取堆快照Heap Snapshot
      • 过滤关键字,查找Detached,结果中的Detached HTML**Element,即为分离DOM
    • 判断是否存在频繁垃圾回收
      • 任务管理器或Timeline中查看内存是否频繁变化

    代码优化

    • 使用基于benchmark.js的jsperf来评估JavaScript代码的性能

    • 慎用全局变量

      • 全局对象定义在全局上下文,一直存在与全局上下文执行栈,直到程序退出才会释放
      • 局部作用域出现同名变量容易出现变量污染
      • 使用局部变量的代码,性能要优于使用全局变量的代码
    • 缓存全局变量

      • 缓存全局变量,再使用缓存的变量进行代码操作,性能要优于直接使用全局对量,但性能差距非常小
    • 通过原型对象添加附加方法

      • 构造函数中添加方法,每个实例对象中都会创建一个相同的方法,占用内存
      • 构造函数原型对象上添加方法,每个实例共享一个方法,性能更好
    • 避开闭包陷阱

      • 闭包在外部作用域访问内部作用域的数据,具有指向内部的引用

      • 闭包外层函数调用完成后,内部函数及变量仍被引用,未被释放,容易引起内存泄露

      • 闭包使用完成后,手动将引用清空(赋值为null)

        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Document</title>
        </head>
        <body>
            <button id="btn">hello</button>
            <script>
                // function foo() {
                //     let btn = document.getElementById('btn')
                //     btn.onclick = function() {
                //         console.log(btn.id)
                //     }
                // }
                // foo()
                function foo() {
                    let btn = document.getElementById('btn')
                    btn.onclick = function() {
                        console.log(this.id)
                    }
                    btn = null
                }
                foo()
            </script>
        </body>
        </html>
        
    • 避免属性访问方法使用

      • JavaScript的面向对象的属性是直接对外暴露的,并不需要属性的访问方法(类似Java的getXXX、setXXX方法)
      • 属性访问方法相当于在构造函数中添加实例方法
      • 直接使用属性访问,在性能上,比使用属性访问方法来访问属性更好
    • For循环优化

      • 对要遍历的数组的length进行提前获取并缓存

        let array = [1, 2, 3, 4, 5]
        
        for (let i = 0, len = array.length; i < len; i++) {
            console.log(i)
        }
        
    • 选择最优的循环方法

      在数据量较大的情况下,执行效率排序

      while > for > forEach > for...of > map > for...in

      • for
      • forEach
        • 无法break或return中断
      • for...of
      • for...in
      • map
      • while
    • 文档碎片(document fragment)优化节点添加

      添加dom节点必然会产生回流和重绘,使用文档碎片,将多次节点添加统一成一次,可以减少回流reflow和重绘repaint

      • 使用 document.createDocumentFragment 创建文档碎片,并将要添加的节点append到文档碎片中,最后append到body
      const fragment = document.createDocumentFragment()
              for (let i = 0; i < 10; i++) {
                  fragment.append(document.createElement('div'))
              }
              document.body.append(fragment)
      
    • 克隆优化节点添加

      • 使用element.cloneNode(false) 通过克隆的方式来创建节点,比使用 document.createEleemnt() 性能好一些,但差距不是特别大
    • 直接字面量替换new Object

    相关文章

      网友评论

          本文标题:学习笔记(六)——JavaScript 性能优化

          本文链接:https://www.haomeiwen.com/subject/rvmvsktx.html