在上一节推荐实践中其实不少方面是与效率有关的,但那些都是语言层次的优化,这一节偏重学习大的方面的优化,好比JavaScript脚本的组织,加载,压缩等等。 固然在此以前,分析一下浏览器的特征仍是颇有意义的。javascript
浏览器的特征
1. 浏览器的渲染过程
在详细讨论脚本文件的优化前,咱们先来看一下浏览器是如何渲染一个HTML页面的。
当浏览器渲染一个HTML页面的时候,它老是从页面的开始位置按顺序向页面末尾依次渲染,当页面遇到引用外部文件的时候(JavaScript脚本,CSS文件等),页面渲染就会停下来等待该外部文件下载完并执行完成,而后继续向下渲染。
浏览器在下载和执行脚本时出现阻塞的缘由在于,脚本可能会改变页面或相关代码,这些操做对后面页面内容形成影响。因而当浏览器遇到<script>标签时,因为当前HTML页面无从获知该JavaScript代码是否会向页面添加内容,或引入其余元素,或甚至移除某些标签元素,所以,这时浏览器会中止处理页面,先执行JavaScript代码,而后再继续解析和渲染页面。
一样的状况也发生在使用src属性加载JavaScript的过程当中,浏览器必须先花时间下载外链文件中的代码,而后解析并执行它。JavaScript执行过程耗时越久,浏览器等待响应用户输入的时间就越长。在这个过程当中,页面渲染和用户交互彻底被阻塞了。css
2. 多线程的浏览器
在上面的过程当中,至少使用到了浏览器的HTTP申请线程,界面渲染引擎线程(大多数浏览器JavaScript解释引擎也包含在这个线程中,这个也解释了为何执行JavaScript的时候会阻塞界面渲染),浏览器事件管理线程,这些线程都是由浏览器建立和管理的,这些线程执行特定的功能(从名称上就能看出来)并互相协做,完成页面的申请,资源的下载,页面的渲染,事件的处理等行为。html
3. 单线程的JavaScript解释引擎
我们再把目光聚焦在JavaScript解释引擎上,毫无疑问,JavaScript脚本就是由它解释执行的。
当咱们执行一个行为的时候,这个行为的功能一般都是由JavaScript引擎和浏览器其它线程协做完成的。
JavaScript解释引擎是单线程的,每一个window(或者说一个页面吧)一个JS线程,既然是单线程的,在某个特定的时刻只有特定的代码可以被执行,并阻塞其它的代码。
与此同时,咱们知道浏览器是事件驱动的 (Event driven) ,浏览器中不少行为是异步(Asynchronized)的,当异步事件发生时,如鼠标点击事件,setTimeout延时触发,Ajax返回,触发回调等,它会建立事件并通知JavaScript引擎把事件回调函数放到执行队列中,等待当前代码执行完成后去执行。JavasSript的任务队列就是由普通函数和回调函数构成的。
这就是JavaScript单线程与回调函数的执行特色,这一点也被后台的NodeJS解释器(原本就是浏览器的解释器,因此同样是确定的)继承了,它们这个方面的特性是一模一样。
到这里,咱们再分析一下同窗们经常使用的setTimeout(func, 0)的做用,从上面的原理来看,setTimeout(func, 0)神奇之处就是告诉JavaScript引擎,在0ms之后把func放到任务队列中,等待当前的代码执行完毕再执行,这里的重点是改变了代码的执行流程,这样就可能完成一些特殊的功能。前端
好了,分析完浏览器,下面开始看看这些方面对脚本影响。java
脚本组织 - 静态结构与动态组织
1. 脚本的位置
咱们知道JavaScript代码是放在script元素中的,能够直接嵌套在该元素中,也能够经过src去引用外部的js文件。 git
咱们看看script元素能够放置的位置。
1). 放置在head区域
经过上面浏览器特征的分析,这个区域不太好,由于会阻塞下面body的渲染。
2). 放置在body区域
经过上面浏览器特征的分析,把脚本放到body的最后是比较好的,由于不会阻塞上面的元素的渲染。 github
2. 脚本的数量
因为每一个<script>标签下载时都会阻塞页面渲染,因此减小页面包含的<script>标签数量有助于改善这一状况。这不只针对外链脚本,内嵌脚本的数量一样也要限制。浏览器在解析HTML页面的过程当中每遇到一个<script>标签,都会因执行脚本而致使必定的延时,所以最小化延迟时间将会明显改善页面的整体性能。
这个问题在处理外链 JavaScript 文件时略有不一样。考虑到HTTP请求会带来额外的性能开销,所以下载单个100Kb的文件将比下载 5个20Kb的文件更快。也就是说,减小页面中外链脚本的数量将会改善性能。
一般一个大型网站或应用须要依赖数个JavaScript文件。您能够把多个文件合并成一个,这样只须要引用一个<script>标签,就能够减小性能消耗。
可是还须要注意的是,若是这个合并的文件过大的话,可能会致使一次的下载时间太长而带来别的问题,这时咱们就要考虑采用别的方法来提升效率了,好比动态加载。 web
3. 动态加载
动态加载,从我我的角度来讲就是按需加载,页面打开的时候先加载必须的一些脚本,而后当须要的时候,再加载后续的脚本,固然了,这些脚本也能够以异步的方式在背后先加载好,当须要的时候直接使用便可。看一个动态加载的例子: api
(function() { var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = 'http://yourdomain.com/script.js'; var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })();
脚本的异步与延迟执行
1. 异步加载
上面同步加载脚本的过程是至关耗时的,若是咱们能同时加载多个脚本文件,并且不阻塞页面的渲染线程,是否是能提升效率呢?这就是异步加载的思路,异步加载又叫非阻塞加载,浏览器在下载执行JavaScript代码的同时,还会继续进行后续页面的处理。当同步严重阻碍产品的可用性的时候,异步是势在必行的,这是软件技术发展的一个基本模式。实现脚本的异步加载有不少方式,下面逐一来看一下。
1). 使用script自身特性
HTML5为<script>标签订义了一个新的扩展属性:async。它可以异步地加载和执行脚本,不由于加载脚本而阻塞页面的加载。可是有一点须要注意,在有async的状况下,JavaScript脚本一旦下载好了就会执行,因此颇有可能不是按照本来的顺序来执行的。若是 JavaScript脚本先后有依赖性,使用async就颇有可能出现错误。看个例子: 浏览器
<script src="demo_async.js" async="async"></script>
与async用于类似功能的另外一个属性是defer,它也是异步的加载脚本,看个例子:
<script src="file.js" defer="defer"></script>
不过只有IE和FirFox支持defer属性,其余的浏览器不支持,因此这里就很少讲了,由于咱们开发的网站可不只仅只能在IE和FireFox上能用。
2). 动态异步加载
把上一个例子中的HTML代码换成JavaScript代码就变成动态异步加载了,就是咱们上面的动态加载的代码,这里再拷贝一遍:
(function() { var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = 'http://yourdomain.com/script.js'; var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })();
可是,这种实现的加载方式在加载执行完以前会阻止onload事件的触发,而如今不少页面的代码都在onload时还要执行额外的渲染工做等,因此仍是会阻塞部分页面的初始化处理。因而把加载的时机放到onload触发后就比较好了,大概就是这样:
(function() { function async_load() { var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = 'http://yourdomain.com/script.js'; var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); } if (window.attachEvent) { window.attachEvent('onload', async_load); } else { window.addEventListener('load', async_load, false); } })();
这里的关键就是挂接事件的那几句代码,这里的实现代码中,它不是当即开始异步加载js,而是在onload事件触发时才开始异步加载。这样就解决了阻塞onload事件触发的问题。
这里须要理解的是onload事件是在页面的全部资源都加载完毕(包括图片)后触发的,这时浏览器的载入进度已中止了。与这个事件相关的另一个事件是DOMContentLoaded事件,这个事件是在页面(document)已经解析完成,页面中的dom元素已经可用是触发的,可是这个时候页面中引用的图片、subframe等可能尚未加载完。
3). 使用Ajax实现加载
提及异步加载,咱们不得不提到Ajax,使用Ajax能够轻松实现异步加载,而使用JQuery的实现无疑更是简单明了。看下面的例子:
// 使用Ajax方式实现异步加载 var xhr = new XMLHttpRequest(); xhr.open("get", "script1.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.type = "text/javascript"; script.text = xhr.responseText; document.body.appendChild(script); } } }; xhr.send(null); // 使用JQuery简化加载过程 // 获取并执行一段脚本的快捷方法:jQuery.getScript( url, [callback] ) // 方法至关于: jQuery.get(url, null, [callback], 'script') $.getScript('../scripts/Sample.js', function(data, textStatus) { alert(data); alert(textStatus); });
这个在总结Ajax与JQuery的着重说过了,再也不啰嗦。
4). 模块化管理 - 解决顺序问题
异步加载,须要将全部js内容按模块化的方式来切分组织,其中每一个js文件就可能存在依赖关系,而异步加载不保证执行顺序,这就是个问题了。解决这个问题常见的就是使用模块化管理脚本,加上异步的特性,就是异步模块化管理。
异步模块化管理的概念,也就是AMD (Asynchronous Module Definition)的设计理念已经在目前较为流行的前端框架中大行其道了,像JQuery, Dojo, MooTools, EmbedJS 这种大型的框架纷纷在其最新版本中加入了对AMD的支持。
同时,一些只注重模块管理的精悍型类库也不断涌现,好比国外的RequireJS框架,它就只使用AMD的特性。使用RequireJS能够帮助用户异步按需的加载JavaScript代码,并解决JavaScript模块间的依赖关系,提高了前端代码的总体质量和性能。
下面这几篇文章讲述了RequireJS框架的用法:
http://www.ibm.com/developerworks/cn/web/1209_shiwei_requirejs/
http://makingmobile.org/docs/tools/requirejs-api-zh/
http://requirejs.org/
而国内的同行们在这方面也在不断努力,终于SeaJS框架横空出世,下面几篇教程就是讲述SeaJS的用法:
http://seajs.org/
http://www.zhangxinxu.com/sp/seajs/docs/zh-cn/index.html
http://www.2cto.com/kf/201312/268256.html
2. 延迟执行
无论是同步加载,仍是异步加载,脚本加载完了就会当即执行,若是咱们想在须要的时候才执行的话,就须要采用一些特殊的手段,看下面的例子:
var se = new Image(); se.onload = registerScript(); se.src = 'http://anydomain.com/A.js';
它利用了图片的src指向的资源是异步下载的特色实现了异步加载,同时利用onload来注册脚本,而后在须要的时候再使用便可。
不过,咱们一般也能够在脚本代码中使用自执行函数把实际的功能封装起来变成一个对象返回回来,这样在须要的时候咱们就能够调用这个对象去完成必定的功能,就像这样:
var app.util = (function() { var f1 = function() {}; var f2 = function() {}; return { F1: f1, F2: f2 }; })();
3. 压缩
这个通常指两个方面:
第一方面:文件自身的压缩
由于脚本等文件(Js, css, html, xml, text, inline script, 一个都不能少)是要在网上传输的,那么脚本中的空格,备注,长变量其实都是对传输效率有害的。因此咱们须要对文件经行压缩。这个是经过相关的工具实现的,通常只要选择成熟的工具,细节就不用咱们管了,好比下列的一些工具:
http://webmedia.blog.163.com/blog/static/416695020123202150472/
http://yui.github.io/yuicompressor/
https://developers.google.com/speed/articles/compressing-javascript?csw=1
https://developers.google.com/closure/compiler/?csw=1
http://www.cnblogs.com/JeffreyZhao/archive/2009/12/09/ikvm-google-closure-compiler.html
第二方面:GZIP压缩
对页面GZIP压缩几乎是每篇讲解高性能WEB程序的几大作法之一,由于使用GZIP压缩能够下降服务器发送的字节数,能让客户感受到网页的速度更快,也减小了对带宽的使用状况。固然,这里也存在客户端的浏览器是否支持它(大多数缘由是因为accept-encoding head头会丢失,因为防火墙,安全过滤软件等缘由)。所以,咱们一般要作的是,若是客户端支持GZIP,咱们就发送GZIP压缩过的内容,若是不支持,咱们直接发送静态文件的内容。服务器端能够配置两套代码,经过accept-encoding的信息来判断。GZIP应该是在部署的时候直接部署压缩过的文件。而不是执行压缩代码来压缩文件。那样会消耗服务器的性能。
不过目前彷佛主流的浏览器默认都是支持GZIP的,因此咱们一般也只须要在服务端开启GZIP压缩就能够了,下面就是一篇教程:
http://www.chinaz.com/web/2012/1017/278682.shtml