前端js性能优化(一)

加载和执行

多数浏览器使用单一进程来处理UI刷新js脚本执行,因此同一时刻只能做一件事。
因此,当页面遇到script标签后都会阻止页面渲染过程并等待脚本的解析和执行,无论脚本是内嵌的还是外链的。

脚本位置

HTML4规范指出script脚本可以放在HTML的head或body中。
如果把script标签放在head中,由于脚本会阻塞页面渲染,因此直到他们全部下载并执行完成后,页面的渲染才会继续。
浏览器在解析到body标签之前,不会渲染页面的任何部分,因此把脚本放到页面顶部将会导致明显的延迟,通常表现为显示空白页面。

大部分浏览器都允许并行下载js文件,因此script标签在下载外部资源时不会阻塞其他script标签。但是仍然会阻塞其他资源的下载,比如图片。

正是由于脚本会阻塞页面其他资源的下载,因此推荐将所有的script标签尽可能放到body标签的底部。

组织脚本

由于每一个script标签都会阻塞页面渲染,所以减少页面包含的script标签数量有助于改善这一状况。这不仅仅针对外链脚本,内嵌脚本的数量也要限制。

无阻塞的脚本

当我们将js脚本合并后,确实只产生一次HTTP请求,但是js的执行还是会锁死浏览器一大段时间。
为了避免这种情况,我们需要向页面中逐步加载js文件,这样做在某种程度上来说不会阻塞浏览器。
无阻塞脚本的秘诀在于,在页面加载完成后才加载js代码。这意味着在window对象的load事件触发后再下载脚本。

实现无阻塞脚本的方式有以下三种:

延迟的脚本defer

HTML4为script标签新定义了一个属性:defer
HTML5则又新增了一个async属性。

带有这两个属性中任一个的脚本文件采用并行下载的方式,即下载过程中不会阻塞页面加载。
这两者的区别在于执行时机,async是下载完成后自动执行,而defer需要等待页面加载完成后执行(具体来说是onload事件之前执行)。

具体也可以看js学习笔记:script元素

动态脚本元素

可以利用DOM动态创建脚本,脚本文件在该元素被添加到页面时开始下载。
这种技术的重点在于,无论何时启动下载,文件的下载和执行过程不会阻塞页面其他进程。你甚至可以将代码放到head区域而不会影响页面其他部分。

通常,把动态script元素添加到head标签中比添加到body中更保险,尤其是在页面加载过程中执行代码时更是如此。
在HTML5中,可以使用document.head直接获取head元素的引用。

由于动态脚本下载完成后会立即执行,因此如果代码中包含其他脚本调用的接口时,就必须要跟踪并确保脚本下载完成并且准备就绪。这可以通过监听script元素的load事件来完成。

var script = document.createElement("script");

script.onload = function(){
    ……
}

script.src = "file1.js";
document.head.appendChild(script);

IE支持另一种实现方式,它会触发一个readystatechange事件。

如果需要的话可以动态加载许多js文件到页面上,但一定要考虑清楚文件的加载顺序。除了firefox和opera能保证脚本会按照你指定的顺序执行,其他浏览器将会按照从服务器端返回的顺序下载和执行代码。
可以用回调函数的形式来确保下载顺序。

动态脚本加载是最通用的无阻塞加载解决方案。

XMLHttpRequest脚本注入

另一种无阻塞脚本的方法是使用XMLHttpRequest对象获取脚本并注入页面中。
也就是通过ajax来获取外部脚本文件,然后通过动态创建script元素将该脚本添加到页面中。

var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function(){
    if(xhr.readyState == 4){
        if(xhr.status >= 200 && xhr.status <= 300 || xhr.status == 304){
            var script = document.createElement("script");
            script.text= xhr.responseText;
            document.head.appendChild(script);
        }
    }
}

xhr.send(null);

这种方法的主要优点是,可以下载js代码但不立即执行。由于代码是在script标签之外返回的,因此它下载后不会自动执行。

这种方法的主要局限性是所请求的js文件必须与所请求的页面处于相同的域。这意味着js文件不能从CDN下载。

推荐的无阻塞模式

向页面中添加大量js的推荐做法:

  • 先添加“动态加载别的代码”所需的代码,比如lazyload等工具js
  • 动态加载初始化页面所需的所有代码

数据存取

计算机科学中的一个经典问题就是通过改变数据的存储位置来获得最佳的读写性能,数据存储的文职关系到代码执行过程中数据的检索速度。

管理作用域

每一个js函数都有一个内部属性Scope,它包含了一个函数被创建时的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
作用域链中的每一个对象被称为一个变量对象。当一个函数被创建时,它的作用域链会被创建此函数的作用域中可访问的数据对象所填充。

而在执行函数时,会创建一个称为执行上下文的内部对象。一个执行上下文定义了一个函数执行时的环境。
函数每次执行时对应的执行上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个执行上下文。
当函数执行完毕,执行上下文就会被销毁。

每个执行上下文都有自己的作用域链,用于解析标识符。
当执行上下文被创建时,它的作用域链初始化为当前运行函数的Scope属性中的对象。这些值按照它们出现在函数中的顺序被复制到当前执行上下文的作用域链中。这个过程一旦完成,一个被称为“活动对象”的新对象就为执行上下文创建好了。
活动对象作为函数运行时的变量对象,包含了所有局部变量、命名参数、arguments以及this。
然后此对象被推入作用域链的最前端。

当执行上下文被销毁,活动对象也随之销毁。
这里写图片描述

在函数执行过程中,每遇到一个变量都会经历一次标识符解析的过程,以决定从哪里获取或存储数据。该过程搜索执行上下文的作用域链,查找同名标识符。

搜索过程从作用域链头部开始,也就是当前运行函数的活动对象。如果找到,就使用这个标识符对应的变量;如果没找到就继续搜索作用域链中的下一个对象。

正是这种搜索过程影响了性能。

标识符解析的性能

在执行上下文的作用域链中,一个标识符所在的位置越深,它的读写速度就越慢。因此函数中读写局部变量总是最快的,而读写全局变量通常是最慢的。

因此在实际操作中,如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量中。

例如在操作DOM的时候,很有可能多次引用全局变量document,搜索该变量的过程必须遍历整个作用域链。
可以先将全局变量的引用存储在一个局部变量中,然后使用这个局部变量代替全局变量。

function(){
    var doc = document;
    var bd = doc.body;
    var links = doc.getElementByTagName("a");
    ……
}

改变作用域链

一般来说一个执行上下文的作用域链是不会改变的,但有两个语句可以在执行时临时改变作用域链:

  • with
  • try-catch

当代码执行到with时,执行上下文的作用域链临时被改变了。一个新的变量对象被创建,它包含了with参数指定的对象的所有属性。
这个对象被推入作用域链的首位,这意味着函数的所有局部变量现在处于第二个作用域链对象中,因此访问的代价更高了。

而当在try代码块中发生错误,执行过程就会自动跳转到catch子句,然后把异常对象推入一个变量对象并置于作用域链的首位。和with相同,在catch代码块内部,函数所有局部变量将会放在第二个作用域链对象中。

动态作用域

无论是with语句、try-catch语句还是eval语句,都被认为是动态作用域。
动态作用域只存在于代码执行过程中,因此无法通过静态分析检测出来。

闭包、作用域和内存

闭包允许函数访问局部作用域之外的数据。
但是使用闭包可能会导致性能问题。
如:

funciton a(){
    var id="111";
    document.getElementById("myId").onclick = function(){
        saveDocument(id);
    }
}

上面例子中的事件处理函数就是一个闭包。
当a函数执行时,一个包含了变量id以及其他数据的活动对象被创建,它成为执行上下文作用域链中的第一个对象,全局对象紧随其后。
当闭包被创建时,它的scope属性被初始化为上面这些对象。

通常来说函数的活动对象会随着执行上下文一同销毁,但由于闭包的scope属性包含了与执行上下文作用域链相同的对象的引用,由于引用还存在于闭包的scope中,因此变量对象无法被销毁。这意味着闭包需要更多的内存开销。

原型链

属性或方法在原型链中的位置越深,访问它的速度越慢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值