javascript优化知识点

2,825 阅读10分钟

1.引用 script标签的优化

多数浏览器都是使用单一的进程来处理用户界面(UI)和JavaScript脚本执行,所以同一时刻只能做一件事,因此就会造成浏览器的堵塞状态,JavaScript脚本执行过程耗时越久,浏览器等待响应的时间就越长。(也就是说当页面加载的时候,遇到script标签的时候,页面加载就会被终止,等到JavaScript脚本执行完毕后,再继续加载页面,这样给用户的体验特别不会,很容易首次加载页面的时候出现白屏状态。)

1.1 传统的方式

传统的方式就是,把JavaScript的脚本放到head标签之后之前,也就把script标签放到最尾处,来确保页面加载完之后才执行JavaScript脚本,从而达到一些不必要的阻塞。(可能这也是我们平时用得比较多的方式)

1.2 延迟JavaScript脚本执行

defer

HTML4为script标签扩展了一个Defer属性,指定这个属性的script标签的脚本不会修改DOM,因此代码能安全的延迟执行。加上这个属性的script标签的脚本可以放在DOM的任意位置,当浏览器加载到它是,它会被下载,但不会被执行,所以不能阻塞页面的方面,它会等到页面加载完成后,在windown的load()函数之前执行。

例子:

    <script type="text/javascript" src="./a.js"defer="defer"></script>
    
    <script type="text/javascript">
    	console.log("b")
    </script>
    
    <script type="text/javascript">
    	window.onload = function () {
    		console.log("c")
    	}
    </script>
    

最终输出的顺序是b,a,c

async

在H5的时候,script又扩展了一个async属性,它与defer相同点就是都是采用并行下载,不会在下载的过程造成页面的阻塞情况;不同点就是,它们的执行时机:async加载完之后,自动执行(也就是说,当加载到它的时候,浏览器可以继续往下加载页面,当它加载完之后,它就会自动执行,而不需要等到页面加载完之后再执行,也不用页面加载等它执行完再往后执行);而dafer要等页面加载完成后才会执行。

2.合拼JavaSript脚本

因为每个script标签初始下载时都会阻塞页面渲染,

所以减少页面包含的script标签数量也有助于页面加载

性能的优化。(如果页面有很多的script标签,每个

script标签都会发送一次http请求(http://tool.oschina.net/jscompress)

,从而浪费了很多没必要的时间,我们可以通过某些在线工具将多

个script标签合拼成一个script标签,最终页面只引用

了一个script,也就是只发送了一次http请求,这样加载性能会比之前加载多个script标签快)

3.作用域/链的优化

JavaScript的作用域同样关系到性能的问题

3.1 尽量少用闭包

在es6之前JavaScript没有什么块级作用域所说,只有全局作用域和函数作用域。(没什么块级作用域有时会给我们带很多莫民奇妙的东西),例如:一个经典的面试题:

//结果输出什么?
for(var i = 0; i<10;i++){
    	setTimeout(function () {
    		console.log(i)
    	},1000)
    }

我们期望的结果是输出0-9。但是结果拼不是我们想这样,这玩(diao)意(mao)既然输出十次10.为什么会这样?因为当setTimeout执行的时候,for循环已经完成,已经变成了10。

怎样解决?(在es6没有到来之前,一般都是同闭包的方法把作用域缓存起来)

for(var i = 0; i<10;i++){
	(function(i){
		setTimeout(function () {
			console.log(i)
		},1000)
	})(i)
}
//结果输出为0-9

最后输出的结果就跟我们期待的一样了,但是上面使用了闭包,闭包也涉及到作用域链的性能问题;因为闭包的属性包含了与执行环境作用域相同的对象的引用,因此导致闭包里面的变量没办法被销毁,从而占用了更多的内存空间,也有可能导致内存泄漏问题,所以使用闭包时还是要谨慎,它关系到内存以及执行速度。

//上面代码优化后
for(let i = 0; i<10;i++){
    	setTimeout(function () {
    		console.log(i)
    	},1000)
    }

3.2 尽量使用局部变量缓存全局变量

在实际开发中尽量使用局部变量缓存全局变量,因为,到一个函数多次访问全局变量的时候,会出现一个作用域练查找的过程,全局作用域位置越深找到的时间就越久,因此这也会涉及到性能的执行速度问题。例如:下面这段简单的代码,在浏览器打开执行看到还是挺快的,没什么问题;但是,它三次引用了obj.name这个全局变量,在这个查找的过程中必须遍历整个作用域链,直到找到为止(幸亏这个全局变量比较浅,幸亏,这段代码只引用了三次;要这个全局变量在window对象上面呢,要是这个全局变量会被引用几千万次呢),然后我们可以将这个多次被引用的全局变量,用局部变量存起来,这样无论这个全局变量被引用多次,它都只会查找一次。


let obj = {
    name:"yuefengsu"
}

function Person() {
    console.log(obj.name +"去吃饭",obj.name +"去睡觉",obj.name +"去打篮球")
}

Person()

//改

let obj = {
    name:"yuefengsu"
}

function Person() {
    let name = obj.name
    console.log(name +"去吃饭",name +"去睡觉",name +"去打篮球")
}

Person()

3.3 尽量不要使用动态作用域

尽量不要使用动态作用域,因为他们会改变执行环境的作用域链。比如with()语句和try{}catch(){}的catch(){}字句...,它们都会改变执行环境的作用域链。比如下面这段代码:with会把一个包含了obj全部属性的新的可变变量置于作用域链的头部,使得访问obj对象属性速度非常快,但是访问局部变量则变慢了,因此还是尽量避免使用。

let obj = {
    name:"yuefengsu"
}
function Person() {
    with(obj){
    console.log(name +"去吃饭",name +"去睡觉",name +"去打篮球")
    }
}
Person()

4.少操作DOM

操作DOM的代价是非常昂贵的,因为浏览器一般都会将DOM文档和Javascript分开来实现的,所以两个互相独立的功能只要通过接口彼此连接,就会产生消耗,而且Javascript要操作Dom会导致浏览器重新计算页面的几何体。我们来打个比方:把DOM和Javascript各自想象为一个岛屿,它们之间由一条桥连接,但是要过这条桥是要收费的,过的次数越多收费就越多。因此我们尽量少过桥,真的非要过桥,也尽量过一次把所以东西都弄过去。

    function innerHTMLLoop(){
        for(let count = 0;count < 15000 ; count++){
            document.getElementById('xxx').innerHTML += 'a'
        }
    }

上面这段代码的,每迭代一次都会操作两次DOM,一次是读取,一次是重写,所以,也就是操作来3W次DOM,这个数字是非常惊人的,在开发中如果遇到类似这样的操作,我们应该利用一个局部变量先将要做的事情缓存起来,等它做完了,再一次性操作DOM。

    function innerHTML(){
        let context = ''
        for(let count = 0 ; count < 15000 ; count ++ ){
            context += 'a'
        }
        doucment.getElementById('xxxx').innnerHTML += context
    }

经过优化后,这段代码只需要操作两次DOM就可以了,是不是比上面的好好多了,我们也节省了15000倍的过桥费。

5.减少重绘和重排

什么是重绘和重排

当DOM树中的变化影响了元素的几何属性(宽和高)-----比如改变边框宽高或者给段落增加文字,导致行数增加----浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受影响的部分失效,并重新构造渲染树。这哥过程称为“重排”。完成重排后,浏览器会重新绘制受影响的部分到屏幕中,这个过程叫“重绘”。所以,也就是说,重排就一定会重绘,重绘,就不会重排,因此,能用重绘解决的问题就不要用重排。

导致重排的因素:

  • 添加或者删除可见的DOM元素
  • 元素位置改变
  • 元素尺寸改变
  • 内容改变
  • 页面渲染器初始化
  • 浏览器窗口尺寸改变

导致强制重排的属性:

  • offsetTop,offsetLeft,offsetWidth,offsetHeight
  • scrollTop,scrollLeft,scrollWidth,scrollHeight
  • clientTop,clientLeft,clientWidth,clientHeight
  • getComputedStyle()

导致重绘因素:

  • 改变背景颜色,字体颜色
  • 改变字体大小
  • 透明度的改变

6.减少不必要的事件绑定

什么是不必要的事件

当页面中存在大量元素,而且每一个都要一次或者多次绑定事件的处理器(比如onclick)时,这种情况可能会影响性能。每绑定一次事件处理器都是有代价的,它要么是加重了页面的负担,要么是增加了运行期的执行时间。需要访问和修改的DOM元素越多,应用程序就越慢,特别是事件绑定通常发生在onload时,此时对每一个富交互应用的网页来说都是一个拥堵的时刻。事件绑定占用了处理时间,而且,浏览器需要跟踪每个事件处理器,这也会占用更多的内存。当这些工作结束时,这些事件处理器中的绝大部分都不再需要,因此有很多工作是没必要的。

处理这些没必要的事件,可以通过事件委托来解决。

所谓的事件委托就是利用每个事件都会经历捕获-到达目标-冒泡三个阶段中的冒泡特性来实现的。

比如给ui下的一万个li绑定事件,要是一个一个的去绑定,这是件恶心的事情,也是没必要的事件。

    document.getElementsByTagName('ui').onclick=function(e){
        let target = e.target || e.srcElement
        if(target.nodeName === "li"){
            dosomethink...
        }
    }

利用事件委托就完美的解决了这个问题,只需要在父元素上绑定一个事件即可。

7.合理的使用循环

循环的方法

  • for
  • do...while
  • while

怎么使用就不说了,这大家都很熟悉。

下面是三个循环的性能测试

function testFor(){
    console.log(new Date().getTime())
    for(let i = 0;i <= 1000; i++){
        console.log(1)
    }
    console.log(new Date().getTime())
}

function testWhile(){
    console.log(new Date().getTime())
    let i = 0
    while(i <= 1000){
        console.log(1)
        i++
    }
    console.log(new Date().getTime())
}

function testDoWhile(){
    console.log(new Date().getTime())
    let i = 0
    do{
        console.log(1)
        i++
    }while(i <= 1000)
    console.log(new Date().getTime())
}

以上可以看出这三种循环性能都挺接近,但是,还是要看业务需要合理的选择循环,也是有利于性能的提升的。

8.合理的使用分支

分支:

  • if...else
  • switch...case

合理的使用不但有利于性能的提升,也有利于代码的可读性。

一般分支比较少的建议使用if...else,多分支的建议使用switch...case,为什么呢,按可读性来说这是可请合理的,按性能来说这也是合情合理的,因为事实上swith的执行速度比if...else要快。

9.少用递归

至于为什么,大家可以看看我之前对递归的一个总结,附上传送门

10.合理使用缓存提高Ajax的请求

有请求就会产生消耗,但是在实际开始这是避免不到的。但是我们可以缓存技术和本地存储来提升AJax性能和不必要的请求。

  • 在服务端,设置HTTP头信息以确保影响被浏览器缓存
  • 在客户端,把获取的数据存在在本地,从而避免再次请求