JavaScript语言层面的优化

JavaScript优化

内存管理

  • 高级语言自带垃圾回收机制
  • 如果不注意内存管理,可能会导致内存泄漏问题。
  • 内存管理:开发者主动申请空间、使用空间和释放空间。JavaScript中并未提供相应的API,由执行引擎根据语言来执行内存管理操作。
    • 申请空间let obj = {};
    • 使用空间obj.name = 'foo';
    • 释放空间obj = null;

垃圾回收与常见GC算法

垃圾回收程序执行时,会阻塞JavaScript的执行。
两种对垃圾的判断:

  • 对象不再被引用时,称为垃圾
  • 对象不能从根上访问时,称为垃圾

可达对象Reachable

  • 定义:从根开始,可以访问到的对象就是可达对象(从引用与作用域链上可访问到)
  • 在Javascript里,根是全局对象(摘自MDN的描述
let obj = { name: 'xm'}; // obj可达对象

let ali = obj; // obj被引用

obj = null; // obj空间被释放,但ali还引用着
function objGroup(obj1, obj2) {
  obj1.next = obj2;
  obj2.prev = obj1;
  return {
    o1: obj1,
    o2: obj2
  }
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'});
// 通过下面的两个delete操作,使得obj1对象不具备可达性,也没有了引用,就会被当做垃圾回收
delete obj.o1;
delete obj.o2.prev;

GC算法

  • GC :垃圾回收机制
  • GC可以找到内存中的垃圾,释放和回收空间
  • GC里的垃圾
    • 程序中不再需要使用的对象
    • 程序中不能再访问到的对象
  • GC算法就是GC查找回收垃圾时遵循的规则
常见GC算法
引用计数:使用判断引用的方式
  • 核心思想:为对象设置引用数,判断引用数是否为0,为0则回收。
  • 引用计数器
  • 引用关系改变时,引用数值会被修改。
优点
  • 发现垃圾立即回收
  • 最大程度减少程序暂停,通过及时释放空间
缺点
  • 无法回收循环引用对象
  • 时间开销大(要时刻监控对象的引用计数)
标记清除:使用可达对象的方式

核心思想:标记 + 清除,两个阶段

  • 遍历所有对象,标记活动对象
  • 遍历所有对象,清除标记,便于垃圾回收
    回收的空间会放在空闲链表中,方便程序申请内存空间。
优点
  • 可以实现对循环引用对象的垃圾回收
缺点
  • 不会立即回收垃圾
  • 空间碎片化,垃圾对象在内存地址上的不连续导致的。
标记整理:标记清除的增强

清除阶段不同:先执行整理,移动对象的位置,让空间可以连续,再清除标记。

分代回收

老生代对象和新生代对象分别采用不同的空间存储与GC回收算法,见V8引擎。

V8引擎

  • 高效执行JavaScript的引擎
  • 采用即时编译,源码编译为字节码,而不是机器码。字节码是机器码的抽象描述,比机器码更节省空间
  • 内存设限:web应用足够使用,且如果太大的话,回收程序执行时阻塞时间太长。
    • 64位操作系统时,不超过1.5G
    • 32位操作系统时,不超过800M

V8引擎的垃圾回收

回收主要指的是引用类型,使用分代回收策略

  • 内存分为新生代、老生代
  • 针对不同对象采用不同算法

V8常用的GC算法有

  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 标记增量
V8的内存回收
  • 内存空间一分为二,左侧小空间为新生代区,右侧大空间为老生代区
  • 小空间存储新生代对象(32M | 16M)
  • 新生代对象指的是存活时间较短的对象(局部作用域的变量)
新生代对象回收
  • 复制算法 + 标记整理
  • 新生代内存区分为两个等大小空间,使用空间为From,空闲空间为To,申请内存时使用From空间
  • 活动对象存储于From空间
  • 标记整理后将活动对象拷贝到To空间,From空间与To空间进行了交换
  • 释放From空间,这样就变成了新的To空间
晋升

新生代对象移动到老生代,两个判断标准

  • 一轮GC后还存活的新生代对象
  • To空间的使用率超过25%,则将To空间的活动对象放到老生代
老生代对象回收
  • 老生代对象存放在右侧老生代区域
  • 64位系统1.4G,32位系统700M
  • 老生代对象是指存活时间较长的对象,如全局下的对象,闭包变量
  • 主要采用标记清除、标记整理、增量标记算法
    • 首先使用标记清除完成垃圾空间回收
    • 采用标记整理算法进行空间优化(当新生代晋升到老生代时,如果空间碎片化导致空间不足是,会执行标记整理)
    • 采用增量标记进行效率优化
增量标记

垃圾回收与JavaScript程序交替执行,避免阻塞JavaScript程序。
标记操作与程序执行交替执行,直到标记完成,然后统一进行清除操作,再进行程序的执行。

Performance工具

意义

对内存使用情况进行监控

使用过程

  • 输入目标网址(但不要打开)
  • 进入开发者工具,选择性能
  • 开启录制功能,访问该网址
  • 执行用户行为,一段时间后停止录制
  • 分析界面中记录的内存信息(需要勾选内存选项)

出现内存问题的体现(在网络环境正常)

  • 页面出现延迟加载或经常性暂停,说明GC频繁进行垃圾回收
  • 页面持续性出现糟糕性能,说明出现内存膨胀
  • 页面的性能随时间越来越差,说明有内存泄漏

监控内存的几种方式

界定内存问题的标准

  • 内存泄漏:内存使用持续升高
  • 内存膨胀:在多数设备上都存在性能问题
  • 频繁垃圾回收:通过内存变化图进行分析
    监控内存的方式
  • 浏览器任务管理器
  • TimeLine时序图记录
  • 堆快照查找分离DOM
  • 判断是否存在频繁的垃圾回收
任务管理器监控内存
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>任务管理器监控内存变化</title>
</head>
<body>
  <button id="btn">add</button>
  <script>
    const oBtn = document.querySelector('#btn');
    oBtn.onclick = function() {
    // 通过点击事件增加JavaScript使用的内存;
      let arrList = new Array(10000000);
    }
  </script>
</body>
</html>

关注内存一栏与JavaScript内存一栏,这两栏信息。内存一栏包含了DOM节点使用的内存,而JavaScript内存一栏只是显示JavaScript使用的内存空间。

TimeLine记录内存
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Timeline记录内存变化</title>
</head>
<body>
  <button id="btn">add</button>
  <script>
    const arrList = [];
    // 模拟大内存消耗情况;
    function test() {
      for(let i = 0; i < 100000; i ++) {
        document.body.appendChild(document.createElement('p'));
      }
      // 将数组元素使用x连接成字符串;
      arrList.push(new Array(1000000).join('x'));
    }
    const oBtn = document.querySelector('#btn');
    oBtn.addEventListener('click', test);
    
  </script>
</body>
</html>
堆快照查找分离DOM

开发者工具中的内存面板
什么是分离DOM

  • 正常DOM节点:界面元素是存活在DOM树上的DOM节点
  • 垃圾对象的DOM节点:DOM节点从DOM树剥离,且没有JavaScript引用
  • 分离状态detached的DOM节点:DOM节点从DOM树剥离,但有JavaScript引用,会驻留在内存中。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>堆快照监控内存</title>
</head>
<body>
  <button id="btn">add</button>
  <script>
    let temEle;
    function fn() {
      let ul = document.createElement('ul');
      for(let i = 0; i < 10; i ++) {
        let li = document.createElement('li');
        ul.appendChild(li);
      }
      temEle = ul;
    }

    const oBtn = document.querySelector('#btn');
    // 点击后,temEle中引用的就是detached DOM;
    oBtn.addEventListener('click', fn);
    
  </script>
</body>
</html>

GC频繁进行垃圾回收的检测方法

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

JavaScript代码优化

精准测试JavaScript的性能

  • 本质上是采集大量的执行样本进行数学统计和分析
  • 使用基于Benchmark.js 的网站来完成
Jsperf使用流程
  • 使用GitHub账号登录
  • 填写个人信息(非必须)
  • 填写详细的测试用例信息(title、slug)
  • 填写准备代码(DOM操作常用)
  • 填写必要有setup和teardown代码
  • 填写具体的测试代码片段

慎用全局变量

  • 全局变量定义在全局执行上下文,是所有作用域链的顶端,查找时间长
  • 全局变量一直存在于上下文执行栈,直到程序退出
  • 局部作用域出现了同名变量时,会遮蔽全局变量
// 使用全局变量
var i, str = '';
for(i = 0; i < 1000; i ++) {
  str += i;
}
// 使用局部变量
for(let i = 0; i < 1000; i ++) {
  let str = '';
  str += i;
}

在jsperf中检查两种方式的性能

缓存全局变量

将使用中无法避免的全局变量缓存到局部。比如在进行DOM查找时,频繁使用document全局变量,就可以让document缓存到局部,使得对document的引用查找起来更快。

通过原型新增方法

通过构造函数挂载在实例上的方法和通过在原型上挂载一样的方法,每一个实例都有一个一样的方法,显然是浪费空间的,并且挂载在原型上的方法,调用起来效率更高。

避开闭包陷阱

闭包使用不当容易造成内存泄漏
下面的代码演示了闭包造成的内存泄漏情况。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>堆快照监控内存</title>
</head>
<body>
  <button id="btn">add</button>
  <script>
    function foo() {
      // 用el变量来引用id为btn的DOM元素
      // 本来该DOM元素在浏览器页面中就存在引用(可以从root节点找到),我们又使用el引用了该DOM节点
      // 当el引用的DOM元素从DOM树中删除时,由于el还存在着该DOM节点的引用,就使得GC无法回收该DOM节点,造成内存泄漏。
      // 因为我们的操作会经常从DOM树中删除节点,一旦操作很多,就会有很多被删除的DOM节点无法回收内存空间
      var el = document.getElementById('btn');
      el.onclick = function() {
        console.log(el.id);
      }
      // 通过el为DOM元素挂载了onclick点击事件处理函数后,将el赋值为null,可以有效阻止闭包造成的内存泄漏
      // el = null;
    }
    // 执行foo()之后,由于id为btn的DOM元素挂载了onclick事件处理函数,函数内部对el.id有引用
    // 所以el变量就成了闭包变量,是无法回收的,但实际上el已经没有什么用处了。
    foo();
  </script>
</body>
</html>

避免属性访问方法的使用

本质上,JavaScript对象的属性都是外部可见,如果使用方法来控制对属性的访问,无疑是增加了一层重定义,没有访问的控制力。

for循环优化

  • 对集合的长度用变量进行缓存,不要每轮循环都去获取。
  • forEach性能最佳、其次是优化后的for循环,最差的是for in 循环

使用字面量而不是构造函数

DOM节点添加的优化

  • 节点的添加操作必定会有回流与重绘
  • 使用文档碎片fragmentdocument.createDocumentFragment();来一次性添加很多的节点,比一次次添加这些节点效率要高,因为回流与重绘的次数大大减少了

克隆行为优化创建节点操作

当新增节点时,可以先从已存在的节点中克隆(该节点与我们想要新增的节点有很多相同的属性等等),比直接新增节点要有效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值