首页 > 编程语言 >JavaScript 性能优化

JavaScript 性能优化

时间:2023-01-09 09:48:51浏览次数:59  
标签:function console log 性能 JavaScript 回收 内存 var 优化

1.内存管理

  • 内存管理介绍

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

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

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

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

  • js中的内存管理

// 申请空间
let obj = {}

// 使用空间
obj.name = 'lg'

// 释放
obj = null

2.JavaScript 中的垃圾回收

  • JavaScript 中的垃圾

    • JavaScript 中内存管理是自动的

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

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

  • JavaScript 中的可达对象

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

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

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

3.GC算法

3.1.概念

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

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

  • GC里的垃圾是什么

    • 程序中不再需要使用的对象
function func() {
	name = 'lg'
	return `${name} is a coder`
}
func()
- 程序中不能再访问到的对象
function func() {
	const name = 'lg'
	return `${name} is a coder`
}
func()
  • GC算法是什么

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

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

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

  • 常见GC算法

    • 引用计数

    • 标记清除

    • 标记整理

    • 分代回收

3.2.引用计数算法

  • 实现原理:

    • 核心思想:设置引用数,判断当前引用数是否为0

    • 引用计数器

    • 引用关系改变时修改引用数字

    • 引用数字为0时立即回收

    • 示例:

const user1 = { age: 11 }
const user2 = { age: 22 }
const user3 = { age: 33 }

const nameList = [user1.age, user2.age, user3.age]
function fn() {
  const num1 = 1
  const num2 = 2
}
fn()
  • 优点:

    • 发现垃圾时立即回收

    • 最大限度减少程序暂停(卡顿)

  • 缺点:

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

    • 时间开销大

// obj1 和 obj2是循环引用对象
function fn() {
  const obj = {}
  const obj2 = {}
  obj1.name = obj2
  obj2.name = obj1
  return 'lg is a coder'
}
fn()

3.3.标记清除算法

  • 实现原理:

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

    • 遍历所有对象找标记活动对象

    • 遍历所有对象清除没有标记对象

    • 回收相应的空间

  • 优点:

    • 解决对象循环引用不能回收的问题
  • 缺点:

    • 释放的空间不连续,也就是空间碎片化

3.4.标记整理算法

  • 实现原理:

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

    • 标记阶段的操作和标记清除一致

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

回收前:

整理后:

回收后:

4.V8

4.1.概念

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

  • V8 采用即时编译

  • V8内存设限

4.2.垃圾回收策略

  • 采用分代回收的思想

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

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

  • 常用的GC算法

    • 分代回收

    • 空间复制

    • 标记清除

    • 标记整理

    • 标记增量

4.3.如何回收新生代对象

  • V8内存分配

    • V8内存空间一分为二

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

    • 新生代指的是存活时间较短的对象

  • 新生代对象回收实现

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

    • 新生代内存区分为两个等大小的空间

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

    • 活动对象存储于From空间

    • 标记整理后将活动对象拷贝至To

    • From空间进行释放

  • 回收细节说明

    • 拷贝过程中可能出现晋升

      • 晋升就是将新生代对象移动到老生代

        • 一轮GC后还存活的新生代需要晋升

        • To空间的使用率超过25%

4.4.如何回收老生代对象

  • 老生代对象说明

    • 老生代对象存放在右侧老生代区域

    • 64位操作系统1.4G,32位操作系统700M

    • 老生代对象就是指存活时间较长的对象

  • 老生代对象回收实现

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

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

    • 新生代晋升时如果发现老生代区域不足,采用标记整理进行空间优化

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

  • 新老生代细节对比

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

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

  • 标记增量:

5.Performance

5.1.为什么使用Performance

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

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

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

  • Performance 提供了多种监控方式

5.2.Performance使用步骤

  • 打开浏览器输入目标网址

  • 进入开发人员工具面板,选择性能

  • 开启录制功能,访问具体页面

  • 执行用户行为,一段时间后停止录制

  • 分析界面中记录的内存信息

5.3.内存问题的体现

  • 页面出现延迟加载或经常性暂停(频繁GC)

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

  • 页面的性能随时间延长越来越差(内存泄漏)

5.4.监控内存的几种方式

  • 界定内存问题的标准

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

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

    • 频繁GC:通过内存变化图进行分析

  • 监控内存的方式

    • 浏览器任务管理器

    • Timeline时序图记录

    • 堆快照查找分离DOM

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

5.5.任务管理器监控内存

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>任务管理器监视内存变化</title>
</head>
<body>
    <button id="btn">Add</button>
<script>
    const oBtn = document.getElementById('btn')
    oBtn.onclick = function () {
      let arrList = new Array(1000000)
    }
</script>
</body>
</html>

在浏览器打开,使用快捷键 Shift + Esc,打开任务管理器,右键选择JavaScript内存选项,点击Add按钮,会发现JavaScript内存会增大

5.6.Timeline记录内存

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>时间线记录内存变化</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'))
      }
      arrList.push(new Array(10000).join('x'));
    }
    document.getElementById('btn').addEventListener('click', test)
</script>
</body>
</html>

打开浏览器,选择性能,录制后可以查看

5.7.堆快照查找分离DOM

  • 什么是分离DOM

    • 界面元素存活在DOM树上

    • 垃圾对象时的DOM节点

    • 分离状态的DOM界面

  • 示例代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>堆快照监控内存</title>
</head>
<body>
    <button id="btn">Add</button>
<script>
    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
    }
    document.getElementById('btn').addEventListener('click', fn)
</script>
</body>
</html>

打开浏览器,选择内存选项,点击页面中的Add按钮,然后获取快照,筛选deta关键字,就可以看到分离DOM

这些分离DOM会消耗内存,所以在我们确定他们没有用的时候,需要将其引用置空tmpEle = null,避免内存浪费。

5.8.判断是否存在频繁GC

  • 为什么要判断

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

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

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

  • 判断方式

    • Timeline中频繁的上升下降

    • 任务管理器中数据频繁的增加减小

6.V8引擎

6.1.执行流程

  • Scanner是扫描器,扫描所有的文件,并且进行一次tokens转化(词法分析)

  • 预解析(PreParser)优点:

    • 跳过未使用的代码

    • 不生成AST,创建无变量引用和声明的scopes

    • 依据规范抛出特定错误

    • 解析速度更快

  • 全量解析(Parser):

    • 解析被使用的代码

    • 生成AST

    • 构建具体scopes信息,变量引用、声明等

    • 抛出所有语法错误

// 声明时未调用,因此会被认为是不被执行的代码,进行预解析
function foo() {
  console.log('foo')
}

// 声明时未调用,因此会被认为是不被执行的代码,进行预解析
function fn() {}

// 函数立即执行,只进行一次全量解析
(function bar(){
  
})()

// 执行foo, 那么需要重新对foo函数进行全量解析,此时foo函数被解析了两次
foo()
  • Ignition:是V8提供的一个解释器,AST转为字节码

  • TurboFan 是VB提供的编译器模块,字节码转为机器码

6.2.堆栈处理

  • 堆栈准备

    • JS 执行环境

    • 执行环境栈(ECStack,execution context stack)

    • 执行上下文

    • VO(G),全局变量对象

6.3.引用类型堆栈处理

6.4.函数堆栈处理

6.5.闭包堆栈处理

6.6.闭包与垃圾回收

继续向下执行

6.7.循环添加事件实现

  • 代码实现
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>循环添加事件</title>
</head>
<body>
    <button index="1">按钮1</button>
    <button index="2">按钮2</button>
    <button index="3">按钮3</button>
    <script src="./15-add-event-loop.js"></script>
</body>
</html>
// 15-add-event-loop.js
var aButtons = document.querySelectorAll('button')
// 有问题的
// for(var i = 0; i < aButtons.length; i++) {
//   aButtons[i].onclick = function (){
//     console.log(`当前索引值为${i}`)
//   }
// }

// 解决1:闭包
for(var i = 0; i < aButtons.length; i++) {
  (function (i){
    aButtons[i].onclick = function (){
      console.log(`当前索引值为${i}`)
    }
  })(i)
}
// 解决2:闭包2
for(var i = 0; i < aButtons.length; i++) {
  aButtons[i].onclick = (function (i){
    return function (){
      console.log(`当前索引值为${i}`)
    }
  })(i)
}
// 解决3:let
for(let i = 0; i < aButtons.length; i++) {
  aButtons[i].onclick = function (){
    console.log(`当前索引值为${i}`)
  }
}
// 解决4:添加自定义属性
for(var i = 0; i < aButtons.length; i++) {
  aButtons[i].myIndex = i
  aButtons[i].onclick = function (){
    console.log(`当前索引值为${this.myIndex}`)
  }
}
// 解决5:事件委托
document.body.onclick = function (ev) {
  var target = ev.target
  var targetDom = target.tagName
  if(targetDom === 'BUTTON') {
    var index = target.getAttribute('index')
    console.log(`当前点击的是第${index}个`)
  }
}
  • 底层执行分析:

有问题的写法分析

闭包写法分析

添加自定义属性的写法分析

7.JavaScript性能优化

7.1. 测试工具JSBench

网站地址:https://jsbench.me/

7.2. 变量局部化

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

7.3.缓存数据

对于需要多次使用的数据进行提前保存,后续进行使用,作用域链查找变快

7.4.减少访问层级

7.5.防抖与节流

  • 为什么需要防抖和节流

    • 在一些高频率事件触发的场景下,我们不希望对应的时间处理函数多次执行
  • 场景:

    • 滚动事件

    • 输入的模糊匹配

    • 轮播图切换

    • 点击操作

  • 浏览器默认情况下都会有自己的监听事件间隔(4~6ms),如果检测到多次事件的监听执行,那么就造成不必要的资源浪费。

  • 前置场景:界面上有一个按钮,我们可以连续多次点击

    • 防抖:对于这个高频的操作来说,我们只希望识别一次点击,可以认为是第一次或者最后一次。

    • 节流:对于高频操作,我们可以自己来设置频率,让本来会执行很多次的事件触发,按照我们定义的频率减少触发的次数。

防抖函数实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>防抖函数实现</title>
</head>
<body>
    <button id="btn">点击</button>
    <script>
        var oBtn = document.getElementById('btn')

        /**
         * 防抖函数:
         * handle 最终需要执行的事件监听
         * wait 事件触发之后多久开始执行
         * immediate 控制执行第一次还是最后一次,false 执行最后一次
         * */
        function myDebounce(handle, wait, immediate){
          // 参数类型判断及默认值处理
          if(typeof handle !== 'function') throw new Error('handle must be a function')
          if(typeof wait === 'undefined') wait = 300
          if(typeof wait === 'boolean') {
            immediate = wait
            wait = 300
          }
          if(typeof immediate !== 'boolean') immediate = false
          // 所谓的防抖效果,我们想要实现的就是可以管理handle的执行次数
          // 如果我们想要执行最后一次,那就意味着无论我们当前点击了多少次,前面的N-1次都是无用的
          let timer = null
          return function proxy(...args) {
            let self = this,
                init = immediate && !timer
            clearTimeout(timer)
            timer = setTimeout(() => {
			  timer = null
              !immediate ? handle.call(self, ...args): null
            }, wait)
            // 如果immediate是true,那就立即执行
            // 如果想要实现只在第一次执行,那init的赋值就再加上timer为null
            // 因为只要timer为null,那么就意味着没有第二次点击
            init ? handle.call(self, ...args): null
          }
        }
        // 定义事件执行函数
        function btnClick(ev){
          console.log('点击了', this, ev)
        }
        // 当我们执行了按钮点击之后就会执行
        oBtn.onclick = myDebounce(btnClick, 200, true)
    </script>
</body>
</html>

节流函数实现:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>节流函数实现</title>
  <style>
    body{
      height: 5000px;
    }
  </style>
</head>
<body>
  <script>
    // 节流:在自定义的一段时间内让事件进行触发
    function myThrottle(handle, wait) {
      if(typeof handle !== 'function') throw Error('handle must be an function')
      if(typeof wait === 'undefined') wait = 400
      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) {
          // 此时就说明是一个非高频次操作,可以执行 handle
          clearTimeout(timer)
          timer = null
          handle.call(self, ...args)
          previous = new Date()
        } else if(!timer){
          // 当我们发现当前系统中有一个定时器了。就意味着我们不需要再开启定时器
          // 此时说明这次的操作发生在了我们定义的频次时间范围内,不应该执行 handle
          // 这个时候可以自己定义一个定时器,让handle在interval之后去执行
          timer = setTimeout(() => {
            clearTimeout(timer) // 这个操作只是将系统中的定时器清除了,但是timer中的值还在
            timer = null
            handle.call(self, ...args)
            previous = new Date()
          }, interval)
        }
      }
    }
    // 定义滚动事件监听
    function scrollFn() {
      console.log('滚动了')
    }
    // window.onscroll = scrollFn
    window.onscroll = myThrottle(scrollFn, 600)
  </script>

</body>
</html>

7.6.减少判断层级

function doSomeThing(part, chapter) {
  const parts=['ES2016', '工程化', 'React', 'Node']
  if(part) {
    if(parts.includes(part)){
      console.log('属于当前前端课程')
      if(chapter > 5) {
        console.log('您需要提供VIP身份')
      }
    }
  } else {
    console.log('请确认模块信息')
  }
}
doSomeThing('ES2016', 6)

// 优化
function doSomeThing(part, chapter) {
  const parts=['ES2016', '工程化', 'React', 'Node']
  if(!part) {
    console.log('请确认模块信息')
    return
  }
  if(!parts.includes(part)) return
  console.log('属于当前前端课程')
  if(chapter > 5) {
    console.log('您需要提供VIP身份')
  }
}
doSomeThing('ES2016', 6)

7.7.减少循环体活动

var test = () => {
  var i
  var arr = ['zxc', 38, '前端']
  for (i = 0; i < arr.length; i++) {
    console.log(arr[i])
  }
}
test()

// 优化1
var test = () => {
  var i
  var arr = ['zxc', 38, '前端']
  var len = arr.length
  for (i = 0; i < len; i++) {
    console.log(arr[i])
  }
}
test()

// 优化2
var test = () => {
  var arr = ['zxc', 38, '前端']
  var len = arr.length
  while(len--) {
    console.log(arr[len])
  }
}
test()

7.8.字面量与构造式

let test = () => {
  let obj = new Object()
  obj.name = 'zxc'
  obj.age = 18
  obj.slogan = '前端'
  return obj
}
console.log(test())
// 优化
let test = () => {
  let obj = {
    name: 'zxc',
    age: 18,
    slogan: '前端'
  }
  return obj
}
console.log(test())

var str2 = new String('zxc')
// 优化
var str1 = 'zxc'

标签:function,console,log,性能,JavaScript,回收,内存,var,优化
From: https://www.cnblogs.com/caicai521/p/17036024.html

相关文章

  • 数学建模之优化与迭代1
    大家好,我是gdut本科生一枚,本文是我的学习笔记,内容来自目前正在学习的章云教授的高等数学课程,视频来源于b站,如有侵权请联系我删除,谢谢。内容写的一般,希望这个博客能帮助大家......
  • C++ 返回值优化RVO
    目录按值返回返回值优化计算性构造函数关闭RVO参考返回值优化(ReturnValueOptimization,简称RVO)是通过对源代码进行转换、消除对象的创建来实现加速程序,提升程序性能的......
  • 客服系统前端开发:JavaScript删除对象数组中指定key value的对象【唯一客服】网页在线
    经常我们有这样的需要,比如有一个对象数组,我们要把这个数组里某个对象删除掉,根据他的某一个key的value来删除可以使用JavaScript的filter()方法来删除对象数组中指定k......
  • 客服系统前端开发:JavaScript获取URL中的协议部分和域名部分【唯一客服】网页在线客服
    再客服系统中如果想要链接websocket需要确定是ws:// 还是wss://所以,我封装了两个函数,用于获取URL中的协议是HTTP还是HTTPS,以及获取到域名部分可以使用JavaScript中......
  • javascript 操作剪切板
    此库优点:支持电脑和手机端浏览器第一步:声明一个对象$(function(){varclipboard=newClipboardJS(document.getElementById("btnCopyFileShareLink"......
  • 硬盘(网络)的性能分析工具
    磁盘的性能指标:吞吐量=IOPS*I/O大小IOPS:平均每秒IO的次数吞吐量:平均每秒IO的数据量磁盘I/O越大,IOPS越高,那么磁盘那么每秒I/O的吞吐量就越高随机读写频繁的应用,如......
  • JavaScript-删除节点,克隆节点,注册事件,删除事件
    JavaScript-删除节点,克隆节点,注册事件,删除事件目录JavaScript-删除节点,克隆节点,注册事件,删除事件5.节点操作5.5删除节点5.6复制节点(克隆节点)5.8三种动态创建元素......
  • JavaScript笔记
    变量作用域:1、全局变量:在全局作用域下声明的变量​ 在函数内部没有声明直接赋值的变量也是属于全局变量全局变量:只有浏览器关闭的时候才会销毁,比较占内存资源局部......
  • 【前端调试】- 借助Performance分析并优化性能
    欢迎阅读本系列其他文章【前端调试】-更好的调试方式VSCodeDebugger【前端调试】-断点调试的正确打开方式介绍首先简单过一下Performance的使用,打开网页点击控制台......
  • 提升你的技能:编写干净高效的 JavaScript 的 7 个技巧
    编写干净的代码对每个开发人员来说都是必不可少的,因为它使代码易于阅读、理解和维护。干净的代码使团队中每个人的生活更轻松,您的代码更不容易出错,并且更容易添加新功能。......