探真无阻塞加载javascript脚本技术,咱们会发现不少意想不到的秘密

  下面的图片是我使用firefox和chrome浏览百度首页时候记录的http请求javascript

 

下面是firefox:css

 

下面是chrome:html

 

  在浏览百度首页前我都将浏览器的缓存所有清理掉,让这个场景最接近第一次访问百度首页的情景。前端

  在firefox的请求瀑布图里有个表现很是之明显:就是javascript文件下载完毕后,有一段时间是没有网络请求被处理的,这段时间事后http请求才会接着执行,这段空闲时间就是所谓的http请求被阻塞。java

  浏览器里的http请求被阻塞通常都是由javascript所引发,具体缘由是javascript下载完毕以后会当即执行,而javascript执行时候会阻塞浏览器的其余行为,例如阻塞其余javascript的执行以及其余的http请求的执行。这样会致使页面加载变慢,若是这个变慢很明显,此时用户操做网页会发现页面没有反应会反应很慢,慢是网站用户体验的梦魇。程序员

  我目前开发的一些系统,在开发环境里常常碰到javascript阻塞页面加载的问题,主要缘由是咱们网站不少静态资源和脚本都被独立抽取在了一台单独的静态资源服务器上,而本地的开发环境模拟的静态资源服务环境经常很不稳定(常常宕机),有时一些新建的脚本没有及时更新到开发环境上,所以某些js脚本文件没法正确获取,这些问题致使页面加载时候这些js脚本就会阻塞页面的加载,此时浏览器会反复尝试请求这些js文件,直到请求超时才会认定该脚本的url无效,若是中途你没法忍受这种等待,强制关闭浏览器的请求,会发如今浏览器控制台里不少脚本都没法找到,这样你就没法在控制台里设置js代码断点调试js,若是等待js加载完毕,时间又会被浪费,无奈之下只要找到那些无效的url将其注释掉,哎,结果好几回都将有注释的错误代码提交到了svn服务器上,这些事情真是很恼人。web

  无论那种浏览器,也无论是新版本仍是老版本的浏览器,它们都秉持浏览器的单线程特性,这个特性彷佛是一个很难撼动的准则,当咱们在浏览器的地址栏里输入一个url地址,访问一个新页面时候,页面展现的快慢就是由一个单线程所控制,这个线程叫作UI线程,UI线程会根据页面里资源(资源是指css文件,图片等等)书写的前后顺序来加载资源,加载资源也就是使用http请求获取资源,像css外部文件,html文件以及图片等资源http请求处理完毕也就意味着资源加载结束,可是像外部的javascript文件则不一样,它的加载过程被分为两步,第一步和加载css文件和图片同样,就是执行一个http请求下载外部的js文件,可是javascript完成http操做后并不意味操做完毕,UI线程接着会执行它,若是你开发的页面里js代码执行时间过长,那么用户就会明显感受到页面的延迟。为何浏览器不能把javascript代码的加载过程拆分为下载和执行两个并行的过程,这样就能够充分利用时间完成http请求,这样不是就能提高页面的加载效率了吗?这个问题的答案固然是否认的,javascript是一个编程语言,js代码是有智力的,它除了能够完成逻辑性的工做,还能够经过操做页面元素来改变页面UI效果,若是咱们忽略javascript对UI的影响,让它延迟执行,结果会形成页面展现的混乱。那么他会产生什么样的混乱呢?这个混乱的描述以下:chrome

  最简单最好理解和最好掌握的思路是线性思路,对于浏览器UI显示要按线性思路理解即放在页面前部的资源会优先加载和执行,浏览器还会认为前一步的内容均可能会是后一步页面展现前提,若是浏览器擅自中止中间某个代码的执行,颇有可能页面最终呈现的效果和设计者设计的不一样,这样咱们就没法开发出正确的页面。并且按线性思路当你碰到页面UI效果出问题时候,你很容易定位问题所在,若是咱们将js代码的加载和执行分隔开来,这就比如把线性思路变成了树状结构,那么你掌握页面加载的思路和解决UI加载问题时候就会变得更加困难,到时不少人都会抓狂和思路混乱,因此我在这里说混乱。编程

  综上所述,js脚本下载和执行是一个完整的操做,是绝对不能被割裂的。跨域

  浏览器为了提高用户体验,加快UI线程的执行是一个没法回避的问题,看来拆分js的下载和执行是不可行的,如是乎浏览器换了种方式,这个方式也就是在同一个时间能下载多个资源,咱们再看上图,在同一个域名下,firefox能够同时下载两种图片,chrome能够同时下载4个静态资源,不过这是针对图片和css文件,对于js文件彷佛仍是一个接着一个的下载,下载一个执行一个,不过在ie8以上版本,js能够和图片同样并行加载,ie这样作就提高了js文件下载的效率,不过到了js执行时候仍是要严格按照顺序执行。

  多个http链接并行下载资源就比如多个线程共同完成某个任务,若是并行http链接更多,那么能有更多http资源同时被下载,可是浏览器提供并行执行的http链接实在太少了,例如上面firefox才两个,chrome也只有4个,那如何突破浏览器的链接个数的限制了?方法很简单就是将经常使用的,稳定的静态资源统一放在一个静态资源服务器上,由统一的域名对外提供,这个域名要和主体请求的域名不同,原理是由于浏览器只经过域名来限制链接的个数,若是一个页面里有两个不一样的域的,那么并行的http请求个数也会变成两倍。这个作法有点讨浏览器的巧,是程序员发现浏览器的特色而总结出的技术,它相似一个hack技术,而hack技术不会是标准技术,因此它确定有它的瓶颈区,因此这样的技术都是会有个度的,浏览器限制请求个数绝对不是平白无故的,咱们看百度页面并行下载图片的http协议的版本都是1.1,http1.1特色就是长链接,长链接的好处是在页面和服务端频繁交互时候效率很好,可是浏览器的页面操做并非老是频繁的请求服务器,而为了加载静态资源而建立不少长链接,服务器会不得不维护大量无用的长链接,给服务器的压力是可想而知的。相比之下http1.0协议,它不使用长链接,而是短链接,所以早期版本的浏览器会给http1.0协议开启的链接数要高于http1.1链接数,所以有些网站将静态资源服务器提供的http协议版本下降到1.0,这样能够有效的提高浏览器的并发链接数,这个作法会给网页性能带来意想不到的提高,不过现代的浏览器彷佛更愿意平等对待两个不一样版本的协议了,新版的浏览器有些将两类协议的并发数变成同样了。而对于处于客户端地位的浏览器维护多个连接对于资源消耗是庞大的,并且域名过多也会增长dns解析的开销,因此并发链接开启越多,并不必定真的会达到提高性能的目的,那么多少个域最合适了?雅虎的前端工程师给了一个答案:2个是最佳的,这个数据怎么得来的我就不太清楚了,不过结果很简单很好用,记住就好了。

  下面我就要聊聊如何解决js阻塞页面加载的问题了,js之因此会阻塞UI线程的执行,是由于js能控制UI的展现,而页面加载的规则是要顺序执行,因此在碰到js代码时候UI线程会首先执行它,而这点不少程序员不知道或者知道但被忽视,所以致使编写代码时候将用于展现的代码和用于处理逻辑的代码混淆在一块儿,这样作的后果是使js代码形成的阻塞更加严重,因此雅虎公司的前端团队提出了一个前端优化的规则:将js脚本放置到html文档的末尾,这样就能有效的避免UI的阻塞。可是这个方法太简单了,不利于咱们对网站进行更加深刻的优化。为了作的深刻,咱们要须要更进一步分析,首先咱们知道js脚本按出现的位置分为两类一类是行内脚本即写在页面里的脚本,一类是js的外部文件,行内脚本的优化比较简单,就是尽可能在页面写更少的代码,就算要写代码也主要是控制页面加载的UI显示的代码,不必的代码就放在外部的js文件里,js外部文件优化就比较复杂,为了精简行内脚本,咱们就不得不将大量的js代码放到外部文件里,早先时候我都会尽可能将全部外部js文件合并成一个js文件,可是如今我发现一个复杂的外部脚本颇有可能会让页面的阻塞状况变得更加的严重,所以外部脚本要根据其功能拆分为展现脚本和逻辑脚本,可是事实上展现代码和逻辑代码很难分离,其实有个更加简单的标准让咱们拆分代码:将全部外部代码分为UI初始化代码和其余代码,,UI初始化代码是在页面加载时候执行的代码,咱们如今只要判断哪些代码在页面加载时候执行就好了,这个标准就容易多了。

  另外,上文我提到过我碰到页面被js阻塞的状况,有时我为了调试js代码会一直等待浏览器判断无效的js加载失败,那么我是怎么判断浏览器已经判断外部js加载无效了?很简单就是查看浏览器忙指示结束,浏览器的忙指示以下图所示:

  忙指示(忙指示现象包括:浏览器选项卡的旋转圆圈,鼠标变成漏斗鼠标,浏览器左下的状态栏显示正在加载某某url以及老版的ie显示页面加载进度的现象)标记结束了,就代表页面的加载操做结束了,为了防止js脚本阻塞页面加载,那么咱们要作到的就是让那些不会用于页面初始化展现的js代码的加载和执行操做在浏览器忙指示结束后触发,为了作到这一点咱们就得知道忙指示结束后会触发什么命令,这个命令就是浏览器的onload事件,所以咱们让那些和页面加载无关的js脚本在onload方法里执行,在onload事件里我就会使用dom技术,构建script节点,设置它的src指向须要加载的脚本路径,而后将这个srcipt节点加入到html文档的head里,为了彻底确保这个js在忙指示结束后执行,我使用setTimeout方法调用动态加载脚本的方法,进一步确保代码在忙指示结束后执行,实践下来感受效果的确不错。

  理解到这里我原本很高兴,认为本身又理解了一个前端开发的难点,而且有一个好的解决方案,可是等我回味一下发现有点不对头,我常用的jQuery定义了ready方法,ready方法会在dom加载完毕后执行,而我本身的方案倒是在onload后执行,代码执行远远落后jQuery的ready方法执行时机,这个感受让我很不舒服,其次,在页面开发里咱们会使用不少第三方库,虽然我如今开发尽可能作到只用jQuery这一个第三方库,可是其余人则不尽然,他们会使用不少第三方库,不少库有大量UI操做的通用方法,这些方法很是好用,常用这些库会致使咱们本身写的控制UI的js代码经常会依赖它们,那么拆分UI控制脚本和其余脚本就无从谈起了。总之,如今web前端开发太依赖第三方库,就算一个牛逼的前端团队,拒绝使用第三方库,什么都本身开发,当web应用变复杂后,通用库和业务代码的耦合度都是很难解决的问题,这也会致使咱们无法真正将UI展现代码和逻辑代码真正的分离。

  个人方案其实知足不了实际生产的需求,不够完美,因此本文到这里没有推导出通用规则,真使人失望,面对上面的新问题那咱们该怎么办了?这个问题不是无解的,现行技术就有它的解决方案,那就是无阻塞脚本的加载。无阻塞加载脚本技术的核心就是:加载js脚本时候,被加载的js脚本不会阻塞UI线程的执行和以阻塞方式加载的脚本。

  下面是无阻塞加载脚本的技术方案:

  XHR Eval

  顾名思义,经过XHR读取脚本,经过Eval令脚本生效。

  代码以下:

var xhrObj = new XMLHttpRequest();
xhrObj.onreadystatechange = function(){
    if(xhrObj.readyState == 4 && 200 == xhrObj.status){
        eval(xhrObj.responseText);
    }
};

xhrObj.open("GET", "A.js", true);
xhrObj.send("");

  

  因为XMLHttpRequest自己不能跨域,因此该方法不能跨域。

  XHR Injection

  使用动态建立script元素,来写入脚本,在某些状况下可能比上一种方法要快些。

  代码以下:

var xhrObj = new XMLHttpRequest();
xhrObj.onreadystatechange = function(){
    if(xhrObj.readyState == 4){
        var scriptElem = document.createElement("script");
        document.getElementsByTagName("head")[0].appendChild(scriptElem);
        scriptElem.text = xhrObj.responseText;
    }
};
xhrObj.open("GET", "A.js", true);
xhrObj.send("");

  

 

  Script in Iframe

  因为Iframe是开销最高的DOM元素,这种方法仍是尽可能避免使用,并且这种方法也没法实现跨域。

  Script DOM Element

  可跨域方案,利用动态插入script元素来让脚本读取、生效。

  代码以下:

var scriptElem = document.createElement("script");
scriptElem.src = "http://anydomain.com/A.js";
document.getElementByTagName("head")[0].appendChild(scriptElem);

  

  Script Defer

  原生方案。利用defer属性来防止脚本阻塞。

  代码以下:

<script defer src="A.js"></script>

  

  不过许多浏览器不支持该属性。

  document.write Script Tag

  动态写脚本的另外一种方案,不过只在IE中是并行下载的。

  代码以下:

document.write("<script type='text/javascript' src='A.js'></script>");

  

 

  script defer和document.write Srcipt Tag不是跨浏览器的方案这里不推荐。
  页面嵌套iframe方案我没有详述,缘由是我如今很讨厌iframe,iframe是dom元素里开销最大的元素,有它就意味着慢,并且我最近碰到一个生产问题就是iframe引发,缘由就是对iframe跨域形成,iframe跨域之后,父窗体和子窗体代码就不能互访了,并且iframe写法的不正确(写的很相似跨站脚本挟持)还会致使浏览器启动默认的安全机制,最终出现用户没法正常使用页面的状况,因此我也不推荐使用iframe。
  xhr eval也是我不会去使用的方式,由于它用eval命令,而eval的使用经常会为黑客留下破坏你网站的漏洞。

  所以最好的方案就是xhr 注入和script dom element了,这两个方案不存在浏览器兼容问题,并且后者还能跨域,不过跨域的选择也是要谨慎的,跨域脚本也会带来隐形的安全风险,无论怎么说这两个方案使用场景基本上能够包括全部阻塞脚本加载的场景。

  注意:无阻塞加载脚本的核心技术就是动态的建立script的dom节点。

  无阻塞脚本加载技术还有个好处就是,那些和页面展现无关的脚本无须非要放在onload事件里执行,它随时随地能够运行简直就是完美。

  不过无阻塞脚本有个很大的隐患,这个隐患是不少会使用无阻塞脚本技术的程序员都会忽视的问题,这个问题就是无阻塞脚本很容易产生“变量未定义”的问题,这个问题的本质就是无阻塞脚本会破坏js脚本加载顺序的问题,当某个脚本依赖于另外一个脚本时候,而另外一个脚本又没有加载执行完毕,最后就会产生“变量未定义”的问题,例如jQuery没有提早加载,所以使用$时候提示$变量未定义。


  那么咱们该如何解决这个问题了,咱们的思路就是让那些依赖于无阻塞加载的脚本的js代码在脚本加载完毕后才会执行,咱们须要一个办法将无序的脚本加载变得有序,上面我推荐的两种方法都是使用dom技术建立script节点,而后将该节点加入到文档的head头部,对于script节点,在非ie浏览器下有一个onload事件,该事件会在script加载完毕后才会执行,ie浏览器下有onreadystatechange事件,而ie下script的dom节点有一个readystate属性,它的取值以下:
  1.uninitialized(未初始化):对象存在还没有初始化;
  2.loading(正在加载):对象正在加载数据;
  3.loaded(加载完毕):对象数据加载完成
  4.interactive(交互):能够操做对象,可是尚未彻底加载;
  5.complete(完成):对象已经加载完毕。
  具体用法以下所示:

scriptNode.onreadystatechange = function(){
	if (scriptNode.readystate == 'complete'){// todo......}
}

  

 

  这个作法就是为dom加载定义了一个回调函数,当dom加载完毕后回调函数才会执行,这样就解决了代码执行顺序的问题了。

  另外还有一个方式就是使用setTimeout,具体使用就是定义一个轮询,判断须要使用的变量是否存在,若是不存在,就继续轮询,若是变量存在则中止轮询,代码模式以下所示:

  代码以下:

function lunxun(){
	if ("undefined" == typeof(XXXX)){
		setTimeout(lunxun,300);
	}else{
		ftn();
	}
}
lunxun();

  

  无阻塞脚本的好处就是不会阻塞UI的执行,也不会影响其余同步js代码的执行,不过无阻塞脚本改变了脚本的加载顺序,因此在使用无阻塞脚本时候必定要更加注意脚本之间的依赖关系,保证整个页面的脚本都能正常执行。

  在之前的文章里我屡次提到了js的模块加载技术,时下流行的模块加载技术有进口货requirejs和国产货seajs,使用这些技术,咱们会发现js文件加载都是按模块加载的,也就是说你页面定义了多少个js模块,那么这个页面就有多少个js文件,刚开始使用它们时候我很诧异,按照前端优化原则http请求越少越好,为何先进的模块技术却会让js文件变得更多了,接着我分析了下它们加载js的请求,终于明白了,它们都使用的无阻塞脚本加载技术,即便用script节点方式加载脚本,这样就很容易屏蔽js带来的阻塞问题了。

  上面的实例中我使用script节点将脚本都是嵌入到head节点里,这个彷佛和将脚本置于html文档末尾的原则不一样,这个是否是须要改进了,答案是不须要改进,将脚本置于文档末尾目的是为了不js的阻塞,而咱们使用无阻塞脚本了,这个问题不是解决了吗?因此代码置于head标签仍是html文档底部也就可有可无了。

  最后我要纠正一个错误的观点,页面加载的总时间是衡量页面加载快捷的标准吗?答案是,的确是个标准,可是不是最精确的标准,页面同步阻塞加载的时间才是衡量页面加载效率的准确标准,非阻塞脚本加载可能会增长整个页面加载的时间,可是它能够减小页面阻塞加载的时间,而页面阻塞才是影响用户体验的元凶,页面优化最重要的关注点就是你所看到的的东西要加载的更加快。

   无阻塞脚本能够分割外部脚本的下载和执行操做,这是程序员使用的hack技术,它很酷,可是会致使程序的复杂度增长,可读性降低,因此它应该是web前端架构师的技术,平常开发咱们要慎用它。

相关文章
相关标签/搜索