资讯专栏INFORMATION COLUMN

【前端性能优化】高性能JavaScript整理总结

zzbo / 2619人阅读

摘要:然后执行环境会创建一个活动对象,活动对象作为函数运行的变量对象,包含所有局部变量命名参数参数集合和,当执行环境销毁,活动对象也被销毁。

高性能JavaScript整理总结

关于前端性能优化:首先想到的是雅虎军规34条
然后最近看了《高性能JavaScript》
大概的把书中提到大部分知识梳理了下并加上部分个人理解
这本书有参考雅虎特别性能小组的研究成果,所以跟34 军规有很多相似之处
有不当之处请在评论区指正,感谢~

约定:很多单词语法都是简写比如doc指document,点点点代表不重要代码省略,码字不易(/双手合十)

1. 加载和执行

JavaScript是单线程,所以JavaScript的加载和执行是从上至下加载执行完一个再继续加载执行下一个文件,会阻塞页面资源的加载,所以一般情况下JavaScript文件放在body标签底部,很多后端开发人员放在body标签外下面,这样做不好的地方有两处:1、不规范 2、可能会造成js获取不到页面元素而导致报错。而放在body标签内底部可以确保js执行前页面渲染完成 


js...                         //正确





js...                         //错误

合并脚本,每个

三种无阻塞下载JavaScript的方法:

1.使用
闭包的[[scope]]属性引用了assignEvents执行环境作用域链的对象(这个对象包含id属性),当执行结束时,执行环境销毁,理应活动对象也被销毁,但是因为闭包的引入,导致这个活动对象处于激活状态,就无法销毁,这就需要更多的内存空间。
由于IE使用非原生JavaScript对象实现DOM对象,所以闭包可能导致内存泄漏。
由于saveDom方法跨作用域访问量变量id,所以闭包会带来性能消耗,解决办法是:常用的跨作用域变量存储在局部变量中使用

对象成员

大部分JavaScript代码是面向对象编写的(自定义对象、BOM/DOM),所以会导致非常频繁的访问对象成员。所以访问对象也有可优化的地方
嵌套成员:对象可以嵌套其他成员
嵌套深度与读取时间成正比

原型链

推荐一个回答,第一个 苏墨橘的回答,相比于之前看的千篇一律的解答,这个更容易理解
关于原型链


3. DOM编程**(常见的性能瓶颈)

三个问题:
1.访问和修改DOM元素
2.修改DOM元素样式导致的重绘和重排
3.通过DOM事件处理与用户交互

DOM

DOM:document object module 文档对象模型,可以理解为操作文档的程序接口
为什么说DOM慢,操作DOM代价昂贵,简单理解就是两个独立的功能只要通过接口彼此连接,就会产生消耗。比如:中国人买iPhone,美国人买卫龙,需要交税的,这个税就是消耗,同样,从DOM到JavaScript或从JavaScript到DOM都有类似的消耗。
所以尽量的减少这种交税的次数来达到一定的性能优化,
最坏的方式就是在循环中操作或者访问DOM,非常消耗性能。

//bad
for(var i = 0; i < 10000; i++){
    document.querySelectorAll("#aaa").innerHTML += "a";
}
//good
var aaaHtml = ""; 
for(var i = 0; i < 10000; i++){
    aaaHtml += "a";
}
document.querySelectorAll("#aaa").innerHTML += aaaHtml;

关于innerHTML和DOM方法(doc.createElement())谁更快

不考虑Web标准的情况下,差不多。
除了最新版的WebKit内核之外的浏览器中,innerHTML更快,旧版本浏览器效率更高
新版的WebKit内核的浏览器DOM方法更快
克隆节点带来的优化效果不是很明显、略过
访问集合元素时使用局部变量(跟操作一个元素多次是一个道理,不赘述)

遍历DOM

一般来说,querySelectorAll()是获取元素最快的API 返回的是一个NodeList
querySelector() 返回的是element,
querySelectorAll()还有一点就是可以同时获取两类元素

var two = doc.querySelectorAll("div.aaa,div.bbb");

重绘和重排

浏览器下载完页面中的所有组件--HTML标记、JavaScript、CSS、图片之后会解析生成两个内部数据结构:
1.DOM树 表示页面结构 比如操场上早操,小红你站这,小绿你站那,小明...(滚出去),哈,开个玩笑,这种位置的结构就像DOM树
2.渲染树
表示DOM节点如何显示 比如 小红穿绿衣服,小绿穿红衣服,小明穿毛呢大衣 小红长头发 小绿绿头发等等

DOM树中每一个需要显示的节点在渲染树种至少存在一个对应的节点(隐藏元素没有对应节点,所以可以利用这一点,先把元素隐藏然后处理然后显示来优化消耗的性能),渲染树中的节点被称为‘帧’或者‘盒’,符合CSS模型定义。(盒子模型不是落地成盒)当DOM和渲染树构建完成,浏览器开始显示页面元素。
那什么时候开始重绘和重排呢:

当DOM变化影响了几何属性,浏览器会让渲染树中受到影响的部分失效,重新构造渲染树。这个过程称为重排; 比如班级座位正常,某段时间后小明狂胖200斤,本来小明坐一个位置,现在需要两个位置,其他同学就需要往两边坐或者往后坐,当然,小明会不会滚出去取决于小明的成绩好坏。
完成重排后,,浏览器会把受影响的部分重新绘制到屏幕上,这个过程称为重绘;
当改变DOM的非几何属性时,只会发生重绘,不会重排;
重绘和重排都是代价昂贵;尽量减少

重排何时发生:
1.添加或删除可见DOM元素
2.元素位置改变
3.元素尺寸改变(内外边距、边框厚宽高等)
4.内容改变 (内容导致尺寸变化的时候)
5.页面渲染器初始化
6.浏览器窗口尺寸变化

减少重绘和重排

//三次重绘
el.style.borderLeft = "1px";
el.style.borderRight = "2px";
el.style.padding = "5px";

//一次重绘
el.style.cssText = "border-left: 1px;border-right: 2px; padding: 5px";

批量修改DOM时如何减少重绘和重排: 步骤:
1.使元素脱离文档流
2.对其应用多重改变
3.把元素带回文档中 //步骤1 3 两次重排

三种方法使DOM脱离文档:
1.隐藏元素--应用修改--显示
2.使用文档片断,在当前DOM之外构建一个子树,再拷回文档
3.拷贝到一个脱离文档的节点中,修改副本,副本替换原始元素

让元素脱离动画流

一般来说,重排只会影响一小部分渲染树,但是也有可能影响很大一部分甚至全部。一次大规模的重排可能会让用户觉得页面一顿一顿的,影响用户体验
避免大部分重排:元素使用绝对定位让其脱离文档流--动画--恢复定位

IE和:hover

从IE7开始,IE可以在任何元素上使用:hover这个伪选择器,但是当你大量元素使用时 会降低响应速度 IE8更明显

事件委托:事件逐成冒泡被父级捕获

每绑定一个事件处理器都是有代价的
事件三阶段:捕获--到达目标--冒泡
事件委托的兼容性问题:访问事件对象、判断事件源、取消冒泡(可选)、阻止默认动作(可选)
使用事件委托来减少事件处理器的数量


4. 算法和流程控制

循环

大多数编程语言中,代码执行时间大部分消耗在循环中,所以循环也是提升性能的重要环节之一

JavaScript四种循环:
1.for循环

Tips:for循环初始化会创建一个函数级变量而不是循环级,因为JavaScript只有函数级作用域(ES6存在块级作用域if(){let n = ...}let定义的n只作用于if块内部,执行完就会释放不会导致变量提升),所以在for循环中定义一个变量和在循环体外定义一个变量时一样的
var i = 100;
for(var i = 0; i < 10; i++){
    console.log(i)  //0,1,2,3...9
}

2.while循环
3.do-while循环
4.for in 循环
Tips:for in循环可以枚举任何对象的属性名(不是值),但是for in比其他三个循环明显要慢,所以除非要迭代一个属性数量未知的对象,否则避免使用for in循环,如果遍历一个属性数量已知属性列表,其他循环比for in快,比如:

    var arr = ["name","age"],
        i = 0;
    while(i < arr.length){
        process(object[arr[i]]);
    }    

假设以上四种循环类型性能一样,可以从两个方面去优化循环的性能:
(当循环体复杂度为X时,优化方案优先减少循环体的复杂度,循环体复杂度大于X时,优化方案优先减少迭代次数 )
1.每次迭代的事务(减少循环体的复杂度)
2.迭代的次数(减少循环的次数,百度‘达夫设备’),可以这么理解,达夫设备就是拆解循环,比如遍历一个长度为100的数组,普通情况下循环体执行100次,达夫设备的思想是把100次拆为每次循环执行多次(n表示)100对n取余,执行取余次数,再执行100除以n(下舍)次循环,这个循环体执行n次普通循环体的操作
达夫设备代码:(这个8就是我说的n)

    var i = items.length % 8;           //先循环余数次数
    while(i){
        process(items[i--]);
    }
    i = Math.floor(items.length / 8);   //再循环8的整数倍次数  循环体是普通循环的8倍 可以写成函数传参调用
    while(i){
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
    }

最小化属性查找:

for(var i = 0, len = arr.length; i < len; i++){
    ...
}

基于函数的迭代:forEach()
forEach遍历一个数组的所有成员,并执行一个函数

arr.forEach(function(value, index, array){
    ...
})

但是所有情况下。基于循环的迭代比基于函数的迭代快8倍,在运行速度要求严格时,基于循环的迭代优先于基于函数的迭代

条件语句

if-else对比switch:
当条件较少时 使用if-else更易读,而当条件较多时if-else性能负担比switch大,易读性也没switch好。
优化if-else的方法是:尽可能的把可能出现的条件放在首位,比如:

    var i = Math.random(1);     
    if(i <= 0.8){            //i小于0.8是几率最大的,如果i的值满足i <= 0.8 后面的条件就不会再判断了
        ...
    }else if(i > 0.8 && i <= 0.9){
        ...
    }else{
        ...
    }

当条件很多的时候:(比如10个和10个以上),避免使用条件语句if-else、switch是最佳方式是使用hash表

Memoization

减少工作量就是最好的性能优化技术(你可以理解为,砍需求是为了性能优化,这是鲁迅说的--鲁迅:这句话我还真说过)
Memoization避免重复工作,缓存前一个计算的结果为后面的计算所用
比如分别求4、5、6的阶乘
求6的阶乘的时候,因为我缓存了5的阶乘结果,那么6的阶乘就是5的阶乘结果乘以6


            function memoizeA(n) {
                if(!memoizeA.cache){
                    memoizeA.cache = {
                        "0": 1,
                        "1": 1
                    }
                }
                if(!memoizeA.cache.hasOwnProperty(n)){
                    memoizeA.cache[n] = n * memoizeA(n-1)
                }
                return memoizeA.cache[n]
            }

            var a1 = memoizeA(4)
            console.log(a1)          //24
            var a2 = memoizeA(5)
            console.log(a2)            //120
            var a3 = memoizeA(6)
            console.log(a3)           //720

            
            function memoize(func, cache) {
                cache = cache || {};
                
                var shell = function (arg) {
                    if(!cache.hasOwnProperty(arg)){
                        cache[arg] = func(arg);
                    }
                    return cache[arg];
                }
                return shell;
            }
            var funCcc = function ccc(n){
                if(n == 0){
                     return 1;
                }else{
                    return n*ccc(n-1)
                }
            }
            var a4 = memoize(funCcc,{"0":1,"1":1});
            console.log(a4(6));         //720

5. 字符串和正则表达式

说明:正则表达式我不会,这里就不说了

字符串

比较下四中字符串拼接方法的性能:
A:str = str + "a"+"b"
B:str += "a" + "b"
C: arr.join("")
D:str.concat("b","c")
对于A与B比较:B会在内存中创建一个临时字符串,字符串拼接为"ab"后赋给临时字符串,临时字符串赋给str;大多数浏览器下A优于B,但在IE8及更早的版本中,B优于A
关于join、concat加前两种拼接的效率:

    //+=
    (function () {
        var startTime = new Date().getTime();
        var str = "";
        var addStr = "hello world~, hello xiaojiejie";
        for(var i = 0; i < 100000; i++){
            str += addStr;
        }
        var endTime = new Date().getTime();
        console.log("字符串str += a:");
        console.log(endTime-startTime);
    })();
    // +
    (function () {
        var startTime = new Date().getTime();
        var str = "";
        var addStr = "hello world~, hello xiaojiejie";
        for(var i = 0; i < 100000; i++){
            str = str + addStr;
        }
        var endTime = new Date().getTime();
        console.log("字符串str = str + a:");
        console.log(endTime-startTime);
    })();
    //concat
    (function () {
        var startTime = new Date().getTime();
        var str = "";
        var addStr = "hello world~, hello xiaojiejie";
        for(var i = 0; i < 100000; i++){
            str = str.concat(addStr);
        }
        var endTime = new Date().getTime();
        console.log("字符串str.concat:");
        console.log(endTime-startTime);
    })();
    //join
    (function () {
        var startTime = new Date().getTime();
        var str = "";
        var arr = [];
        var addStr = "hello world~, hello xiaojiejie";
        for(var i = 0; i < 100000; i++){
            arr.push(addStr);
        }
        str = arr.join("");
        var endTime = new Date().getTime();
        console.log("字符串join:");
        console.log(endTime-startTime);
    })();

我用这段代码简单在chrome65上测试了下,平均下来A>B>C>D,未统计取平均,也没测试其他浏览器
书上说在IE老版本join是比较快的,也是大量字符串拼接的唯一高效方式
详细参考 几种字符串拼接性能


6. 快速相应的用户界面

浏览器UI线程

用于执行JavaScript和更新用户界面的进程被称为"浏览器UI线程",UI线程的工作基于一个队列系统,当进程空闲时,就会从改队列提取任务去执行,该任务可能是JavaScript代码也可能是UI更新(重绘、重排)。
UI:用户界面 GUI:图形用户界面 这张图来自 链接

浏览器限制JavaScript任务的运行时间,限制两分钟,可以防止恶意代码不断执行来锁定你的浏览器
单个JavaScript操作的花费总时间应该小于等于100ms,这就意味着在100ms内响应用户的操作,不然就会让用户感受到迟钝感

定时器让出时间片断

如果代码复杂100ms运营不完,可以使用定时器让出时间片断,从而使UI获得控制权进行更新。

这个例子只是说明JavaScript单线程,定时器可以把任务放到后面执行,方便理解
console.log(111);
setTimeout(func(){console.log(222)},0);
console.log(333);
//111 333 222

JavaScript是单线程,所以定时器可以把JavaScript任务放到后面,控制权先交给UI线程
定时器精度有几毫秒的偏差,,Windows系统中定时器的分辨率为25ms,所以建议延迟最小值设置为25ms

把一个任务分解成一系列子任务
把一个运行时间长的函数分解为一个个短时间运行的子函数

使用时间戳计算获得程序运行时间,以便快速找到运行时间较长的代码部分进行优化

重复的定时器会抢夺UI线程的运行时间,1秒及以上的低频定时器不会有什么影响,当使用高频100ms-200ms之前的定时器时响应会变慢,所以高频重复定时器使用要注意

Web Workers (HTML5新特性)

在UI线程外运行,不占用UI线程的时间
来自W3C的worker demo
Web Workers不能修改DOM
运行环境组成:
一个navigator对象
一个location对象(与window.location相同 属性-只读)
一个self对象,指向worker对象
可以引入需要用到的外部文件importScripts()方法
可以使用js对象 Object、Array、Date等
XHR
定时器
close() 立刻停止Worker运行

W3C介绍Web Worker
博文:Web Worker原理和应用介绍
实际应用场景:处理纯数据或者与UI线程无关的长时间运行脚本,个人觉得大量的纯计算可以考虑使用


7. Ajax(阿炸克斯)

前面说到数据存取会影响性能,理所应当的,数据的传输同样影响性能
Ajax通过异步的方式在客户端和服务端之间传输数据。

数据传输

请求数据的五种方式:

A:XMLHTTPRequest(简称XHR)
最常用异步异步发送和接收数据,包括GET和POST两种方式
不能跨域
GET--参数放在url后面,请求得到的数据会被缓存,当url加参数超过2048,可以使用POST方式
POST--参数在头信息,数据不会被缓存
XHR工作原理及优缺点参考选我选我

B:动态脚本注入
其实就是创建一个script元素这个元素的src不受当前域限制,但是不能设置请求头信息,也就是只能用GET方式

C.Multipart XHR
MXHR荀彧一个HTTP请求就可以传输多个数据
通过在服务端讲资源打包成一个双方约定的字符串分割的长字符串发送到客户端,然后根据mime-typed类型和传入的其他头信息解析出资源
缺点:资源不能被缓存

D.iframe
E.comet

发送数据:XHR、Beacons、

数据格式

A.XML
优点:通用、格式严格、易于验证
缺点:冗长、结构复杂有效数据比例低

B.JSON
JSON.parse():JSON-->对象
JSON.stringify():js值-->JSON字符串
文件小、下载快、解析快

C.JSON-P
在客户端注册一个callback, 然后把callback的名字传给服务器。此时,服务器先生成 json 数据。 然后以 javascript 语法的方式,生成一个function , function 名字就是传递上来的参数 jsonp。最后将 json 数据直接以入参的方式,放置到 function 中,这样就生成了一段 js 语法的文档,返回给客户端。

D.HTML
E.自定义数据格式

Ajax性能

最快的Ajax请求就是没有请求(贫一句:最快的写程序方式就是天天跟产品拌嘴,砍需求,那啥,我先跑了,产品拿着刀追来了)

避免不必要的请求:
服务端设置HTTP头信息确保响应会被浏览器缓存
客户端讲获取的信息存到本地避免再次请求(localstorage sessionstorage cookice)
设置HTTP头信息,expiresgaosu告诉浏览器缓存多久
减少HTTP请求,合并css、js、图片资源文件等或使用MXHR
通过次要文件用Ajax获取可缩短页面加载时间


8. 编程实践

避免双重求值

eval()、Function慎用,定时器第一个参数建议函数而不是字符串都能避免字符串双重求值

使用对象或者数组直接量

直接量:

var obj = {
    name:...
    age:...
}

非直接量:

var obj = new Object()
obj.name = ...
...

运行时直接量比非直接量快

避免重复工作

A:延迟加载(懒加载)
进入函数-->判断条件-->重写函数
B:条件预加载
函数调用前提前进行条件检测
var addEvent = doc.addEventListener ? funcA : funcB

使用JavaScript速度快的部分

A.位操作
B.原生方法,首先原生方法是最快的,而且浏览器会缓存部分原生方法
C.复杂计算时多使用Math对象
D.querySelector和querySelectorAll是查询最快的
当用Document类型调用querySelector()方法时,会在文档元素范围内查找匹配的元素;而当用Element类型调用querySelector()方法时,只会在这个元素的后代元素中去查找匹配的元素。若不存在匹配的元素,则这两种类型调用该方法时,均返回null。


9. 构建并部署高性能JavaScript应用

这一章讲的都是其他章节的优化原理的实践,主要有:
1.合并多个js文件
2.预处理js文件
3.js压缩
4.js的HTTP压缩
5.缓存js文件
6.处理缓存问题
7.使用内容分发网络(CDN)这个有点效果显著的感觉,前年第一次用的时候感觉快了很多,打个比方就是:
京东网上水果蔬菜超市,假设你在上海买了一个榴莲,京东可以在上海的仓库给你发货,如果上海没有他们的仓库,就在离你最近的一个仓库发货,以保证最快速度送到你手上(吃什么不好,吃榴莲,别人会说食屎拉你)。这个仓库放的就是静态资源文件,根据请求发出的位置找到最近的CDN节点把资源返回给请求端,大概是这个意思,具体原理参考CDN原理
现在很多方式都在gulp、webpack工具里进行了,方便省事


10. 工具

JavaScript性能分析

使用Date对象实例减去另一个实例获得任务运行时间毫秒数

匿名函数

测量分析匿名函数的方法就是给匿名函数加上名字

调试工具

个人比较喜欢chrome调试工具
贡献几个比较全的教程
基础篇
优化篇
实战1
实战2
英文使用介绍

脚本阻塞

Safari4、IE8、Firefox3.5、chrome及以上允许脚本并行下载,但阻塞运行,虽然文件下载快了,但是页面渲染任会阻塞直到脚本运行完
对运行慢的脚本进行优化或重构,不必要的脚本等到等到页面渲染完成再加载

Page Speed

显示解析和运行JavaScript消耗的时间,指明可以延长加载的脚本,并报告没被使用的函数

Fiddler

Fiddler是一个HTTP调试代理工具,能检测到网络中所有资源,以定位加载瓶颈

YSlow

YSlow工具可以深入观察页面初始加载和运行过程的整体性能

WebPagetest

WebPagetest:根据用户浏览器真实的连接速度,在全球范围内进行网页速度测试,并提供详细的优化建议。
WebPagetest

Google PageSpeed

PageSpeed 根据网页最佳实践分析和优化测试的网页。

Pingdom 网站速度测试

输入 URL 地址,即可测试页面加载速度,分析并找出性能瓶颈。
Pingdom 网站速度测试

还有很多类似工具:参考前端性能优化和测试工具总结


本文档主干内容来自于《高性能JavaScript》及其他其他博客并注明出处,如有侵权请联系作者删除~
后续会通过举证说明更多方案的效果,不断完善此文档

注:内容有不当或者错误处请指正~转载请注明出处~谢谢合作!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/54525.html

相关文章

  • 前端性能优化】高性能JavaScript整理总结

    摘要:然后执行环境会创建一个活动对象,活动对象作为函数运行的变量对象,包含所有局部变量命名参数参数集合和,当执行环境销毁,活动对象也被销毁。 高性能JavaScript整理总结 关于前端性能优化:首先想到的是雅虎军规34条然后最近看了《高性能JavaScript》大概的把书中提到大部分知识梳理了下并加上部分个人理解这本书有参考雅虎特别性能小组的研究成果,所以跟34 军规有很多相似之处有不当之...

    bovenson 评论0 收藏0
  • 前端知识点整理

    摘要:难怪超过三分之一的开发人员工作需要一些知识。但是随着行业的饱和,初中级前端就业形势不容乐观。整个系列的文章大概有篇左右,从我是如何成为一个前端工程师,到各种前端框架的知识。 为什么 call 比 apply 快? 这是一个非常有意思的问题。 作者会在参数为3个(包含3)以内时,优先使用 call 方法进行事件的处理。而当参数过多(多余3个)时,才考虑使用 apply 方法。 这个的原因...

    Lowky 评论0 收藏0
  • 前端知识点整理

    摘要:难怪超过三分之一的开发人员工作需要一些知识。但是随着行业的饱和,初中级前端就业形势不容乐观。整个系列的文章大概有篇左右,从我是如何成为一个前端工程师,到各种前端框架的知识。 为什么 call 比 apply 快? 这是一个非常有意思的问题。 作者会在参数为3个(包含3)以内时,优先使用 call 方法进行事件的处理。而当参数过多(多余3个)时,才考虑使用 apply 方法。 这个的原因...

    snowLu 评论0 收藏0
  • 前端每周清单年度总结与盘点

    摘要:前端每周清单年度总结与盘点在过去的八个月中,我几乎只做了两件事,工作与整理前端每周清单。本文末尾我会附上清单线索来源与目前共期清单的地址,感谢每一位阅读鼓励过的朋友,希望你们能够继续支持未来的每周清单。 showImg(https://segmentfault.com/img/remote/1460000010890043); 前端每周清单年度总结与盘点 在过去的八个月中,我几乎只做了...

    jackwang 评论0 收藏0

发表评论

0条评论

zzbo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<