JavaScript提高加载和执行效率的方法
前言
无论当前JavaScript代码是内嵌还是在外链文件中,页面的下载和渲染都必须停下来等待脚本执行完成。JavaScript执行过程耗时越久,浏览器等待响应用户输入的时间就越长。浏览器在下载和执行脚本时出现阻塞的原因在于,脚本可能会改变页面或JavaScript的命名空间,它们对后面页面内容造成影响。
一个典型的例子就是在页面中使用document.write()。
JavaScript代码内嵌示例
<html> <head> <title>SourceExample</title> </head> <body> <p> <scripttype="text/javascript"> document.write("Todayis"+(newDate()).toDateString()); </script> </p> </body> </html>
当浏览器遇到<script>标签时,当前html页面无从获知JavaScript是否会向<p>标签添加内容,或引入其他元素,或甚至移除该标签。因此,这时浏览器会停止处理页面,先执行JavaScript代码,然后再继续解析和渲染页面。同样的情况也发生在使用src属性加载JavaScript的过程中,浏览器必须先花时间下载外链文件中的代码,然后解析并执行它。在这个过程中,页面渲染和用户交互完全被阻塞了。
脚本位置
HTML4规范指出<script>标签可以放在HTML文档的<head>或<body>中,并允许出现多次。web开发人员一般习惯在<head>中加载外链的JavaScript,接着用<link>标签用来加载外链的CSS文件或者其他页面信息。
低效率脚本位置示例
<html> <head> <title>SourceExample</title> <scripttype="text/javascript"src="script1.js"></script> <scripttype="text/javascript"src="script2.js"></script> <scripttype="text/javascript"src="script3.js"></script> <linkrel="stylesheet"type="text/css"href="styles.css"> </head> <body> <p>Helloworld!</p> </body> </html>
然而这种常规的做法却隐藏着严重的性能问题。在清单2的示例中,当浏览器解析到<script>标签(第4行)时,浏览器会停止解析其后的内容,而优先下载脚本文件,并执行其中的代码,这意味着,其后的styles.css样式文件和<body>标签都无法被加载,由于<body>标签无法被加载,那么页面自然就无法渲染了。因此在该JavaScript代码完全执行完之前,页面都是一片空白。
由于脚本会阻塞页面其他资源的下载,因此推荐将所有<script>标签尽可能放到<body>标签的底部,以尽量减少对整个页面下载的影响。
推荐的代码放置位置示例
<html> <head> <title>SourceExample</title> <linkrel="stylesheet"type="text/css"href="styles.css"> </head> <body> <p>Helloworld!</p> <!--Exampleofefficientscriptpositioning--> <scripttype="text/javascript"src="script1.js"></script> <scripttype="text/javascript"src="script2.js"></script> <scripttype="text/javascript"src="script3.js"></script> </body> </html>
这段代码展示了在HTML文档中放置<script>标签的推荐位置。尽管脚本下载会阻塞另一个脚本,但是页面的大部分内容都已经下载完成并显示给了用户,因此页面下载不会显得太慢。这是优化JavaScript的首要规则:将脚本放在底部。
组织脚本
由于每个<script>标签初始下载时都会阻塞页面渲染,所以减少页面包含的<script>标签数量有助于改善这一情况。这不仅针对外链脚本,内嵌脚本的数量同样也要限制。浏览器在解析HTML页面的过程中每遇到一个<script>标签,都会因执行脚本而导致一定的延时,因此最小化延迟时间将会明显改善页面的总体性能。
这个问题在处理外链JavaScript文件时略有不同。考虑到HTTP请求会带来额外的性能开销,因此下载单个100Kb的文件将比下载5个20Kb的文件更快。也就是说,减少页面中外链脚本的数量将会改善性能。
通常一个大型网站或应用需要依赖数个JavaScript文件。您可以把多个文件合并成一个,这样只需要引用一个<script>标签,就可以减少性能消耗。文件合并的工作可通过离线的打包工具或者一些实时的在线服务来实现。
需要特别提醒的是,把一段内嵌脚本放在引用外链样式表的<link>之后会导致页面阻塞去等待样式表的下载。这样做是为了确保内嵌脚本在执行时能获得最精确的样式信息。因此,建议不要把内嵌脚本紧跟在<link>标签后面。
无阻塞的脚本
减少JavaScript文件大小并限制HTTP请求数在功能丰富的Web应用或大型网站上并不总是可行。Web应用的功能越丰富,所需要的JavaScript代码就越多,尽管下载单个较大的JavaScript文件只产生一次HTTP请求,却会锁死浏览器的一大段时间。为避免这种情况,需要通过一些特定的技术向页面中逐步加载JavaScript文件,这样做在某种程度上来说不会阻塞浏览器。
无阻塞脚本的秘诀在于,在页面加载完成后才加载JavaScript代码。这就意味着在window对象的onload事件触发后再下载脚本。有多种方式可以实现这一效果。
延迟加载脚本
HTML4为<script>标签定义了一个扩展属性:defer。Defer属性指明本元素所含的脚本不会修改DOM,因此代码能安全地延迟执行。defer属性只被IE4和Firefox3.5更高版本的浏览器所支持,所以它不是一个理想的跨浏览器解决方案。在其他浏览器中,defer属性会被直接忽略,因此<script>标签会以默认的方式处理,也就是说会造成阻塞。然而,如果您的目标浏览器支持的话,这仍然是个有用的解决方案。
defer属性使用方法示例
<scripttype="text/javascript"src="script1.js"defer></script>
带有defer属性的<script>标签可以放置在文档的任何位置。对应的JavaScript文件将在页面解析到<script>标签时开始下载,但不会执行,直到DOM加载完成,即onload事件触发前才会被执行。当一个带有defer属性的JavaScript文件下载时,它不会阻塞浏览器的其他进程,因此这类文件可以与其他资源文件一起并行下载。
任何带有defer属性的<script>元素在DOM完成加载之前都不会被执行,无论内嵌或者是外链脚本都是如此。清单5的例子展示了defer属性如何影响脚本行为:
defer属性对脚本行为的影响
<html> <head> <title>ScriptDeferExample</title> </head> <body> <scripttype="text/javascript"defer> alert("defer"); </script> <scripttype="text/javascript"> alert("script"); </script> <scripttype="text/javascript"> window.onload=function(){ alert("load"); }; </script> </body> </html>
这段代码在页面处理过程中弹出三次对话框。不支持defer属性的浏览器的弹出顺序是:“defer”、“script”、“load”。而在支持defer属性的浏览器上,弹出的顺序则是:“script”、“defer”、“load”。请注意,带有defer属性的<script>元素不是跟在第二个后面执行,而是在onload事件被触发前被调用。
如果您的目标浏览器只包括InternetExplorer和Firefox3.5,那么defer脚本确实有用。如果您需要支持跨领域的多种浏览器,那么还有更一致的实现方式。
HTML5为<script>标签定义了一个新的扩展属性:async。它的作用和defer一样,能够异步地加载和执行脚本,不因为加载脚本而阻塞页面的加载。但是有一点需要注意,在有async的情况下,JavaScript脚本一旦下载好了就会执行,所以很有可能不是按照原本的顺序来执行的。如果JavaScript脚本前后有依赖性,使用async就很有可能出现错误。
动态脚本元素
文档对象模型(DOM)允许您使用JavaScript动态创建HTML的几乎全部文档内容。<script>元素与页面其他元素一样,可以非常容易地通过标准DOM函数创建:
通过标准DOM函数创建<script>元素
varscript=document.createElement("script"); script.type="text/javascript"; script.src="script1.js"; document.getElementsByTagName("head")[0].appendChild(script);
新的<script>元素加载script1.js源文件。此文件当元素添加到页面之后立刻开始下载。此技术的重点在于:无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。您甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的HTTP连接)。
当文件使用动态脚本节点下载时,返回的代码通常立即执行(除了Firefox和Opera,他们将等待此前的所有动态脚本节点执行完毕)。当脚本是“自运行”类型时,这一机制运行正常,但是如果脚本只包含供页面其他脚本调用调用的接口,则会带来问题。这种情况下,您需要跟踪脚本下载完成并是否准备妥善。可以使用动态<script>节点发出事件得到相关信息。
Firefox、Opera,Chorme和Safari3+会在<script>节点接收完成之后发出一个onload事件。您可以监听这一事件,以得到脚本准备好的通知:
通过监听onload事件加载JavaScript脚本
varscript=document.createElement("script") script.type="text/javascript"; //Firefox,Opera,Chrome,Safari3+ script.onload=function(){ alert("Scriptloaded!"); }; script.src="script1.js"; document.getElementsByTagName("head")[0].appendChild(script);
InternetExplorer支持另一种实现方式,它发出一个readystatechange事件。<script>元素有一个readyState属性,它的值随着下载外部文件的过程而改变。readyState有五种取值:
微软文档上说,在<script>元素的生命周期中,readyState的这些取值不一定全部出现,但并没有指出哪些取值总会被用到。实践中,我们最感兴趣的是“loaded”和“complete”状态。InternetExplorer对这两个readyState值所表示的最终状态并不一致,有时<script>元素会得到“loader”却从不出现“complete”,但另外一些情况下出现“complete”而用不到“loaded”。最安全的办法就是在readystatechange事件中检查这两种状态,并且当其中一种状态出现时,删除readystatechange事件句柄(保证事件不会被处理两次):
通过检查readyState状态加载JavaScript脚本
varscript=document.createElement("script") script.type="text/javascript"; //InternetExplorer script.onreadystatechange=function(){ if(script.readyState=="loaded"||script.readyState=="complete"){ script.onreadystatechange=null; alert("Scriptloaded."); } }; script.src="script1.js"; document.getElementsByTagName("head")[0].appendChild(script);
大多数情况下,您希望调用一个函数就可以实现JavaScript文件的动态加载。下面的函数封装了标准实现和IE实现所需的功能:
通过函数进行封装
functionloadScript(url,callback){ varscript=document.createElement("script") script.type="text/javascript"; if(script.readyState){//IE script.onreadystatechange=function(){ if(script.readyState=="loaded"||script.readyState=="complete"){ script.onreadystatechange=null; callback(); } }; }else{//Others script.onload=function(){ callback(); }; } script.src=url; document.getElementsByTagName("head")[0].appendChild(script); }
此函数接收两个参数:JavaScript文件的URL,和一个当JavaScript接收完成时触发的回调函数。属性检查用于决定监视哪种事件。最后一步,设置src属性,并将<script>元素添加至页面。此loadScript()函数使用方法如下:
loadScript()函数使用方法
loadScript("script1.js",function(){ alert("Fileisloaded!"); });
您可以在页面中动态加载很多JavaScript文件,但要注意,浏览器不保证文件加载的顺序。所有主流浏览器之中,只有Firefox和Opera保证脚本按照您指定的顺序执行。其他浏览器将按照服务器返回它们的次序下载并运行不同的代码文件。您可以将下载操作串联在一起以保证他们的次序,如下:
通过loadScript()函数加载多个JavaScript脚本
loadScript("script1.js",function(){ loadScript("script2.js",function(){ loadScript("script3.js",function(){ alert("Allfilesareloaded!"); }); }); });
此代码等待script1.js可用之后才开始加载script2.js,等script2.js可用之后才开始加载script3.js。虽然此方法可行,但如果要下载和执行的文件很多,还是有些麻烦。如果多个文件的次序十分重要,更好的办法是将这些文件按照正确的次序连接成一个文件。独立文件可以一次性下载所有代码(由于这是异步进行的,使用一个大文件并没有什么损失)。
动态脚本加载是非阻塞JavaScript下载中最常用的模式,因为它可以跨浏览器,而且简单易用。
使用XMLHttpRequest(XHR)对象
此技术首先创建一个XHR对象,然后下载JavaScript文件,接着用一个动态<script>元素将JavaScript代码注入页面。清单12是一个简单的例子:
通过XHR对象加载JavaScript脚本
varxhr=newXMLHttpRequest(); xhr.open("get","script1.js",true); xhr.onreadystatechange=function(){ if(xhr.readyState==4){ if(xhr.status>=200&&xhr.status<300||xhr.status==304){ varscript=document.createElement("script"); script.type="text/javascript"; script.text=xhr.responseText; document.body.appendChild(script); } } }; xhr.send(null);
此代码向服务器发送一个获取script1.js文件的GET请求。onreadystatechange事件处理函数检查readyState是不是4,然后检查HTTP状态码是不是有效(2XX表示有效的回应,304表示一个缓存响应)。如果收到了一个有效的响应,那么就创建一个新的<script>元素,将它的文本属性设置为从服务器接收到的responseText字符串。这样做实际上会创建一个带有内联代码的<script>元素。一旦新<script>元素被添加到文档,代码将被执行,并准备使用。
这种方法的主要优点是,您可以下载不立即执行的JavaScript代码。由于代码返回在<script>标签之外(换句话说不受<script>标签约束),它下载后不会自动执行,这使得您可以推迟执行,直到一切都准备好了。另一个优点是,同样的代码在所有现代浏览器中都不会引发异常。
此方法最主要的限制是:JavaScript文件必须与页面放置在同一个域内,不能从CDN下载(CDN指"内容投递网络(ContentDeliveryNetwork)",所以大型网页通常不采用XHR脚本注入技术。
总结
减少JavaScript对性能的影响有以下几种方法:
通过以上策略,可以在很大程度上提高那些需要使用大量JavaScript的Web网站和应用的实际性能。
补充js加载函数:
functionloadJs(url,callback,charset){ varhead=document.getElementsByTagName("head")[0]; varscript=document.createElement("script"); if(!!charset)script.charset="utf-8"; script.src=url; script.onload=script.onreadystatechange=function(){ varf=script.readyState; if(f&&f!="loaded"&&f!="complete")return; script.onload=script.onreadystatechange=null; head.removeChild(script)if(callback){ callback()||callback }; }; head.appendChild(script); }
//js同步加载 functiongetScripts(i,linkArray,fn){ env||getEnv(); varscript=document.createElement('script'); script.type='text/javascript'; script.src=linkArray[i]; varhead=document.head||document.getElementsByTagName('head')[0]; head.appendChild(script); if(env.ie&&'onreadystatechange'inscript&&!('draggable'inscript)){//ie浏览器使用以下方式加载 script.onreadystatechange=function(){ if(/loaded|complete/.test(script.readyState)){ script.onreadystatechange=null; if(i===linkArray.length-1){ if(fn){ fn(); } }else{ getScripts(++i,linkArray,fn); } } }; }else{ script.onload=function(){ if(i===linkArray.length-1){ if(fn){ fn(); } }else{ getScripts(++i,linkArray,fn); } }; } }
//js存在依赖关系依次加载 getScripts(0,[ 'http://caibaojian.com/demo/base.js', 'http://caibaojian.com/demo/reset.js'],function(){ alert('callback'); });
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。