在上一节推荐实践中其实很多方面是与效率有关的,但那些都是语言层次的优化,这一节偏重学习大的方面的优化,比如JavaScript脚本的组织,加载,压缩等等。 当然在此之前,分析一下浏览器的特征还是很有意义的。
浏览器的特征
1. 浏览器的渲染过程
在详细讨论脚本文件的优化前,我们先来看一下浏览器是如何渲染一个HTML页面的。
当浏览器渲染一个HTML页面的时候,它总是从页面的开始位置按顺序向页面末尾依次渲染,当页面遇到引用外部文件的时候(JavaScript脚本,CSS文件等),页面渲染就会停下来等待该外部文件下载完并执行完成,然后继续向下渲染。
浏览器在下载和执行脚本时出现阻塞的原因在于,脚本可能会改变页面或相关代码,这些操作对后面页面内容造成影响。于是当浏览器遇到<script>标签时,由于当前HTML页面无从获知该JavaScript代码是否会向页面添加内容,或引入其他元素,或甚至移除某些标签元素,因此,这时浏览器会停止处理页面,先执行JavaScript代码,然后再继续解析和渲染页面。
同样的情况也发生在使用src属性加载JavaScript的过程中,浏览器必须先花时间下载外链文件中的代码,然后解析并执行它。JavaScript执行过程耗时越久,浏览器等待响应用户输入的时间就越长。在这个过程中,页面渲染和用户交互完全被阻塞了。
2. 多线程的浏览器
在上面的过程中,至少使用到了浏览器的HTTP申请线程,界面渲染引擎线程(大多数浏览器JavaScript解释引擎也包含在这个线程中,这个也解释了为什么执行JavaScript的时候会阻塞界面渲染),浏览器事件管理线程,这些线程都是由浏览器创建和管理的,这些线程执行特定的功能(从名称上就能看出来)并互相协作,完成页面的申请,资源的下载,页面的渲染,事件的处理等行为。
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放到任务队列中,等待当前的代码执行完毕再执行,这里的重点是改变了代码的执行流程,这样就可能完成一些特殊的功能。
好了,分析完浏览器,下面开始看看这些方面对脚本影响。
脚本组织 - 静态结构与动态组织
1. 脚本的位置
我们知道JavaScript代码是放在script元素中的,可以直接嵌套在该元素中,也可以通过src去引用外部的js文件。
我们看看script元素可以放置的位置。
1). 放置在head区域
通过上面浏览器特征的分析,这个区域不太好,因为会阻塞下面body的渲染。
2). 放置在body区域
通过上面浏览器特征的分析,把脚本放到body的最后是比较好的,因为不会阻塞上面的元素的渲染。
2. 脚本的数量
由于每个<script>标签下载时都会阻塞页面渲染,所以减少页面包含的<script>标签数量有助于改善这一情况。这不仅针对外链脚本,内嵌脚本的数量同样也要限制。浏览器在解析HTML页面的过程中每遇到一个<script>标签,都会因执行脚本而导致一定的延时,因此最小化延迟时间将会明显改善页面的总体性能。
这个问题在处理外链 JavaScript 文件时略有不同。考虑到HTTP请求会带来额外的性能开销,因此下载单个100Kb的文件将比下载 5个20Kb的文件更快。也就是说,减少页面中外链脚本的数量将会改善性能。
通常一个大型网站或应用需要依赖数个JavaScript文件。您可以把多个文件合并成一个,这样只需要引用一个<script>标签,就可以减少性能消耗。
但是还需要注意的是,如果这个合并的文件过大的话,可能会导致一次的下载时间太长而带来别的问题,这时我们就要考虑采用别的方法来提高效率了,比如动态加载。
3. 动态加载
动态加载,从我个人角度来说就是按需加载,页面打开的时候先加载必须的一些脚本,然后当需要的时候,再加载后续的脚本,当然了,这些脚本也可以以异步的方式在背后先加载好,当需要的时候直接使用即可。看一个动态加载的例子:
(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
JavaScript大杂烩17 - 性能优化