概述
- 性能优化是不可避免的
- 提高运行效率,降低运行开销的行为即为性能优化
内容概要
- 内存管理
- 垃圾回收与常见的GC算法
- V8引擎的垃圾回收
- Performance工具
内存管理
- 为什么需要内存管理 内存泄漏
- 内存:有可读写的单元组成,表示一篇可操作的空间
- 管理:人为的去操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
- 管理流程:申请-使用-释放
js中的内存管理
- 申请内存空间
- 使用内存空间
- 释放内存空间
// 申请空间
let obj = {}
// 使用
obj.name = 'lg'
// 释放
obj = null
js中的垃圾回收
-
js中的内存管理是自动的
-
什么是垃圾
- 对象不再被引用时是垃圾
- 对象不能从根上访问
-
可以访问到的对象就是可达对象(引用、作用域链)
-
可达的标准就是从根上出发是否能够被找到
-
js中的根可以理解为全局变量对象
js中的引用与可达
let obj = {name:'xm'}
let ali = obj // 引用数值发生了改变
obj = null // 引用数值减少 但是对象依然可达
标记清楚算法
function objGroup(obj1,obj2){
obj1.next = obj2
obj2.prev = obj1
return{
o1:obj1,
o2:obj2
}
}
let obj = objGroup({name:'obj1'},{name:'obj2'})
console.log(obj)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vnq8gpP5-1647770008088)(JavaScript性能优化/image-20220309205729563.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHe2XDz4-1647770008092)(JavaScript性能优化/image-20220309210343343.png)]
GC算法介绍
- GC定义与作用
- GC就是垃圾回收机制的简写
- GC可以找到内存中的垃圾、并释放和回收空间
- 程序中不在需要使用的对象
function func(){
name = 'lg'
return `${name} is a coder`
}
func()
// 从需求角度考虑
// 执行完函数 不在需要name name为垃圾 但是回收的时机是不确定的
- 程序中不能再访问到的对象
function func(){
const name = 'lg'
return `${name} is a coder`
}
func()
// 另一个角度
// 函数执行完 在外部空间中不能在访问到name
GC算法是什么
- GC是一种机制,垃圾回收器完成具体的工作
- 工作的内容就是查找垃圾释放空间、回收空间
- 算法就是工作时查找和回收所遵循的规则
常见GC算法
- 引用计数
- 标记清楚
- 标记整理
- 分代回收
引用计数算法实现原理
- 核心思想:设置引用数,判断当前引用数是否为零
- 引用计数器
- 引用关系改变时修改引用数字
- 引用数字为零时立即回收
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()
引用计数的优点
- 发现垃圾立即回收
- 最大限度减少程序的暂停
引用计数算法的缺点
- 无法回收循环引用的对象
- 时间开销大
// 对象的循环的引用
function fn(){
const obj1 = {} // 引用计数为1
const obj2 = {} // 引用计数为1
obj1.name = obj2 // 引用计数2
obj2.name =obj1 // 引用计数为2
return 'lg is a coder'
}
fn()
// 函数执行完obj1和obj2都不能被访问了,但是obj1和obj2不能被垃圾回收
标记清除算法的实现原理
- 核心思想:分标记和清除两个阶段完成
- 遍历所有的对象找到标记活动对象(可达对象)
- 遍历所有对象清除没有标记的对象(清除所有标记)
- 回收相应的空间
标记清除的优缺点
- 优点: 循环引用不能回收
- 缺点:存储空间碎片化
- 缺点:不能立即回收空间
标记整理算法
常见GC算法总结
- 引用计数
- 标记清除
- 标记整理
V8
- V8是一款主流的JavaScript执行引擎
- V8采用即时编译
- V8内存设有上线 64为1.5G 32位800M
- 为了浏览器制造的,对于网页应用足够试用
- 垃圾回收时在1.5G的时候采用增量标记算法为50毫秒,采用非增量标记算法需要1秒
V8垃圾回收的策略
- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NesqdkQt-1647770008093)(JavaScript性能优化/image-20220310213610594.png)]
V8中常用的GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
V8内存分配
- 白色部分为新生代对象(64为操作系统32M|32为操作系统16M)
- 红色部分为老生代对象
- 新生代对象存活时间较短的对象
- 如何界定存活时间,局部作用域的存活时间较短,全局作用域存活时间较长
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CEOF6fxy-1647770008095)(JavaScript性能优化/image-20220310214633872.png)]
新生代对象的回收实现
- 回收过程采用复制算法+标记整理
- 新生代内存区分为两个等大小空间(from|to)
- 使用空间为From,空闲空间为To
- (第一次代码运行变量)活动对象存储于From空间
- (运行完后)标记整理后将活动对象拷贝自To
- From与To交换空间完成释放From
回收细节说明
- 拷贝过程中可能出现晋升
- 晋升就是将新生代对象移动至老生代
- 某些新生代对象经过一轮GC还存活的新生代需要晋升
- 再拷贝至To空间时发现To空间的使用率超过25%,将此次的活动对象移动至老生代
- 25% from空间和To需要进行交换
V8回收老生代对象的回收
- 老生代存放在右侧老生代区域
- 64位1.4G\32位700M
- 老生代对象就是指存活时间较长的对象
- 全局变量下的一些对象
- 闭包里的数据
老年代对象的回收实现
- 主要采用标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 采用标记整理进行空间优化
- 采用增量标记进行效率优化
细节对比
- 新生代区域垃圾回收使用空间换时间
- 老生代区域垃圾回收不适合复制算法 老生代存储区比较大、存储量比较多
- 增量标记 交替执行程序和回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kMmFsnqp-1647770008096)(JavaScript性能优化/image-20220312161228509.png)]
V8总结
- V8是一款主流的JavaScript执行引擎
- V8内存有上限
- V8采用基于分代回收思想实现垃圾回收
- V8内存分为新生代和老生代
- V8垃圾回收常见的GC算法
Performance工具介绍
- GC的目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用
- 时刻关注才能确定是否合理
- Performance提供多种监控方式
通过使用Performance时刻监控
performance使用步骤
- 打开浏览器输入目标网址
- 开发人员工具面板,选择性能
- 开启录制功能,访问具体界面
- 执行用户行为,一段时间后停止录制
- 分析界面中记录的内存信息
内存问题的体现
- 页面出现延迟加载或经常性暂停
- 页面持续性出现糟糕的性能
- 内存膨胀 所谓的内存膨胀程序为了达到一定的使用速度,申请了一定的内存空间,但是这个内存空间的大小远超过了,当前设备本身所能提供的内存空间
- 页面的性能随时间延长越来越差
- 内存泄漏:内存的持续升高
- 内存膨胀:在多数设配都存在性能问题
- 频繁垃圾回收:通过内存变化图进行分析
常见的监控内存的几种方式
- 浏览器任务管理器
- Timeline时序图记录
- 判断是否存在频繁的垃圾回收
- 堆快照查找分离DOM
任务管理器监控内存
- shift+esc 调出当前程序自带的任务管理器
- 鼠标右击功能选着
任务管理器面板说明
- 内存:原生内存,dom节点所占据的内存
- Javascript内存:js的堆,小括号里的值是所有可达对象我们正在使用的(内存大小)js对象
Timeline记录内存
开发者工具–> 性能–>录制–>模拟用户行为–>
堆块照查找分离DOM
堆快照,找到js堆,进行照片的留存
什么是分离DOM
- 界面元素存活在DOM树上
- 垃圾对象时的DOM节点 (节点不在当前dom树上,js中也没有人在引用)
- 分离状态的DOM节点 (不在当前dom树上,但是js有引用)
开发者工具---->内存---->快照---->deta(分离dom)
确定应用是否存在频繁的垃圾回收
- Timeline中频繁的上升下降
- 任务管理器中数据频繁的增加减小
V8引擎执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tcy8VtJZ-1647770008098)(JavaScript性能优化/image-20220313131327854.png)]
-
Scanner是一个扫面器 词法分析
-
Parser 是一个解析器(AST语法树)预解析,全量解析
- 预解析,很多声明,但并没有使用
- 跳过未被使用的代码
- 不生成AST,创建无变量引用和声明的scopes
- 依据规范抛出特定错误
- 解析速度更快
- 直接全部解析
- 解析被使用的代码
- 生成AST
- 构建具体scopes信息,变量引用、声明等
- 抛出所有语法错误
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z60sZALp-1647770008100)(JavaScript性能优化/image-20220313132719459.png)]
- 预解析,很多声明,但并没有使用
-
ignition 是V8提供的一个解释器,生成字节码
-
TurboFan V8提供的编译器模块,执行字节码
堆栈操作(没听懂)
- js执行环境(V8转为机器码)
- 执行环境栈(ECStack,esecution context stack)
- 全局执行上下文(区分不同的代码 EC(G))execution context
- VO(G),全局变量对象(全局变量对象)
- GO(全局对象)
引用类型堆栈处理
函数堆栈的处理
闭包与垃圾回收
1、 浏览器都有自有的垃圾回收(内存管理,V8为例)
2、 栈空间、堆空间
3、 堆:当前堆内存如果被占用,就不能被释放掉,但是如果我们确认后续不再使用这个内存里的数据,也可以自己主动置空,然后浏览器就会对期进行回收。
4、 栈:当前上下文中是否有内容,被其它上下文的变量变量所占用,如果有则无法释放(闭包)
循环添加事件
var aButtons = document.querySelectorAll('button')
console.log(aButtons)
for(var i = 0;i<aButtons.length;i++){
(function(i){
aButtons[i].onclick = function(){
console.log(`当前索引值为${i}`)
}
})(i)
}
for(var i=0;i<aButtons.length;i++){
aButtons[i].onclick = (function(i){
return function(){
}
})(i)
}
for(let i=1;i<aButtons.length;i++){
aButtons[i].onclick = function(){
console.log(`当前索引值为${i}`)
}
}
for(let i=1;i<aButtons.length;i++){
aButtons[i].myIndex = i
aButtons[i].onclick = function(){
console.log(`当前索引值为${this.myIndex}`)
}
}
底层执行分析
事件委托
页面结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rca0xKtP-1647770008102)(JavaScript性能优化/image-20220319174108234.png)]
js代码
document.body.onclick = function(eve){
var target = ev.target,
targetDom = target.tagName
if(targetDom==='BUTTON'){
var index = target.getAttribute('index')
console.log(`当前点击的是第${index}个`)
}
}
变量局部化
var i, str = ''
function packageDom(){
for(i=0;i<1000;i++){
str +=i
}
}
// 局部化 更优
function packageDom(){
let str=''
for(let i=0;i<1000){
str +=i
}
}
缓存数据
将数据放在,当前作用域中,减少不必要的声明和语句数
缓存数据(作用域链查找变快)
减少访问层级
// 减少访问层级
var obj = {
age:18,
methods:{
m1:{
name:'',
time:100
},
m2:{
name:'',
}
}
}
function Person(){
this.name = 'zce'
this.age = 40
}
let p1 = new Person()
console.log(p1.age)
// 速度慢一点
function Person(){
this.name = 'zce'
this.age = 40
this.getAge = function(){
return this.age
}
}
let p1 = new Person()
console.log(p1.getAge())
防抖与节流
/**
*为什么需要防抖和节流
* 在一些高频率事件触发的场景下我们不希望对应的事件处理函数多次执行
* 场景:
* 滚动事件
* 输入的模糊匹配
* 轮播图切换
* 点击操作
* ...
* 浏览器默认情况下都会有自己的监听事件间隔(4~6ms),如果监测到多次事件的监听执行,那么就会照
* 成不必要的资源浪费
*
* 前置场景:界面上有一个按钮,我们可以连续多次点击
* 防抖:对于这个高频的操作来说,我们只希望识别一次点击,可以认为是第一次或者是最后一次
* 节流:对于高频操作,我们可以自己来设置频率,让本来会执行很多次的事件触发,按着我们定义的频率
* 减少触发的次数
*/
防抖函数 执行最后一次
function myDebounce(handle,wait,immediate){
if(typeof handle !== 'function')throw new Error('handle must be an function')
if(typeof wait === 'undefined')wait = 300
if(typeof immediate === 'boolean'){
immediate = wait
}
if(typeof immediate !=== 'boolean')immediate=false
// 所谓的防抖效果我们想要实现的就是有一个“人”可以管理handle的执行次数
let timer = null
// 如果我们想要执行最后一次,那就意味着无论我们当前点击了多少次,前面的N-1次都无用
return function proxy(...args){
let self = this // this events 参数的处理
clearTimeout(timer)
timer = setTimeout(()=>{
handle.call(self,...args) // this events 参数的处理
},wait)
}
}
防抖函数 执行第一次
function myDebounce(handle,wait,immediate){
if(typeof handle !== 'function')throw new Error('handle must be an function')
if(typeof wait === 'undefined')wait = 300
if(typeof immediate === 'boolean'){
immediate = wait
}
if(typeof immediate !=== 'boolean')immediate=false
// 所谓的防抖效果我们想要实现的就是有一个“人”可以管理handle的执行次数
let timer = null
// 如果我们想要执行最后一次,那就意味着无论我们当前点击了多少次,前面的N-1次都无用
return function proxy(...args){
let self = this, // this events 参数的处理
init = immediate && !timer
clearTimeout(timer)
timer = setTimeout(()=>{
timer = null
!immediate? handle.call(self,...args) : null// this events 参数的处理
},wait)
init ? handle.call(self,...args):null
}
}
节流函数
// 在自定义的时间内让事件触发
function myThrottle(handle,wait){
if(typeof handle !=='function')throw new Error('handle must be an function')
if(typeof wait === 'undefined') wait = 400
let previous = 0 // 定义变量记录上一次执行时的时间
return function proxy(){
let now = new Date() // 定义变量记录当前次执行的时刻时间点
let self = this
let interval = wait -(new - previous)
let timer = null
if(interval <=0){
clearTimeout(timer)
timer = null
handle.call(self,...args)
previous = new Date()
}else if(!timer) {
// 此时的操作发生在指定频率之间不应该执行
// 定义一个计时器 让handle在interval之后执行
timer = setTimeout(()=>{
clearTimeout(timer)
timer = null
handle.call(self,...args)
previous = new Date()
},interval)
}
}
}
// 定义滚动事件监听
function scrollFn(){
console.log('滚动了')
}
window.onscroll = scrollFn
减少判断层级
改变算法
减少循环体活动
var test = ()=>{
var i
var arr = ['zce',38,'我为前端而活']
for(i=0;i<arr.length;i++){
console.log(arr[i])
}
}
var test = ()=>{
var i
var arr = ['zce',38,'我为前端而活']
var len = arr.length
for(i=0;i<len;i++){
console.log(arr[i])
}
}
var test=()=>{
var i
var arr = ['zce',38,'我为前端而活']
var len = arr.length
while(len--){
console.log(arr[len])
}
}
字面量与构造式
字面量优于构造式
}
}
}
// 定义滚动事件监听
function scrollFn(){
console.log(‘滚动了’)
}
window.onscroll = scrollFn
# 减少判断层级
改变算法
# 减少循环体活动
```js
var test = ()=>{
var i
var arr = ['zce',38,'我为前端而活']
for(i=0;i<arr.length;i++){
console.log(arr[i])
}
}
var test = ()=>{
var i
var arr = ['zce',38,'我为前端而活']
var len = arr.length
for(i=0;i<len;i++){
console.log(arr[i])
}
}
var test=()=>{
var i
var arr = ['zce',38,'我为前端而活']
var len = arr.length
while(len--){
console.log(arr[len])
}
}
字面量与构造式
字面量优于构造式