高性能JavaScript之加载和执行
JS在浏览器中的性能,可以认为是开发者所面临的最重要的可行性问题。这个问题因JS的阻塞特性变得复杂,也就是说当浏览器在执行JS代码时,不能同时做其他任何事情。事实上,大多数浏览器都使用单一进程来处理UI(用户界面)更新和JavaScript脚本执行,所以同一时刻只能做其中一件事情。JS执行过程耗时越久,浏览器等待响应用户输入的时间就越长。
从基础层面来说,这意味着<script>标签每次出现都霸道地让页面等待脚本的解析和执行。无论当前的JS代码是内嵌的还是在外链文件中,页面的下载和渲染都必须停下来等待脚本的执行完成。这在页面生存周期中是必要的,因为脚本执行过程中可能会修改页面的内容。一个典型的例子就是在页面中使用document.write()(经常用来显示广告)。
例如:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Script Example</title> </head> <body> <p> <script> document.write("The date is "+(new Date()).toDateString()); </script> </p> </body></html>
当浏览器遇到<script>标签时,当前的HTML页面无从获知JS是否会向<p>标签添加内容,或引入其他元素,或关闭该标签。因此,这时浏览器会停滞处理页面,先执行JS代码,然后再继续解析和渲染页面。同样的情况也发生在使用src的属性加载JS的过程中,浏览器必须先花时间下载外链文件中的代码,然后解析并执行它。在这个过程中,页面渲染和用户交互完全被阻塞了。
1.1脚本位置
这里先说说HTML4规范,HTML4规范指出<script>标签可以放在HTML文档的<head>或<body>中,并允许出现多次。按照惯例,<script>标签用来加载出现在<head>中的外链JS文件中,挨着的<link>标签用来加载外部CSS文件或其他页面元信息。也就是说,把与样式和行为有关的脚本放在一起,并先加载它们,使得页面能够显示正确的外观和交互。
例如:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Script Example</title> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="file1.js"></script> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="file2.js"></script> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="file3.js"></script> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <p>Hello World </p> </body></html>
这些看似正常的代码实际上有十分严重的性能问题:在<head>中加载了三个JS文件。由于脚本会阻塞页面的渲染,直到它们全部下载并执行完成后,页面的渲染才会继续。
因此页面的性能问题会很明显。请记住,浏览器在解析到<body>标签之前,不会渲染页面的任何部分。把脚本放到页面顶部将会导致明显的延迟,通常表现为显示空白页面,用户无法浏览内容,也无法与页面进行交互。
所以通常建议像JS脚本一般都放在</body>前,也就是页面最底下,而CSS文件放在<head></head>之间。虽然说CSS文件过大也会导致延迟,但是这种延迟是可以接受的,如果是JS脚本与CSS脚本放在<head></head>之间如上面的代码所示,那样的话,延迟会显得十分明显。因此推荐<script>标签尽可能放到<body>标签底部,</body>标签之前,以尽量减少对整个页面下载的影响。
例如:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Script Example</title> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <p>Hello World </p> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="file1.js"></script> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="file2.js"></script> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="file3.js"></script> </body></html>
记得在《高性能网站建设》这本书,其中提到的建议之一:就是将脚本放在底部。
1.2组织脚本
由于每个<script>标签初始下载时,都会阻塞页面渲染,所以减少页面包含的<script>标签数量有助于改善这一情况。这不仅仅针对外链脚本,内嵌脚本的数量同样也要限制。浏览器在解析HTML页面的过程中每遇到一个<script>标签,都会因执行脚本而导致一定的延时,因此最小延迟时间将会明显改善页面的总体性能。
一般情况下,组织脚本不单单是将JS文件中的注释或者其他无关紧要的内容去掉,而且也要将其压缩,通过YUI或者是将多个JS文件合并压缩成一个大的JS文件。只需引用一个<script>标签,就可以减少性能的损耗(主要是减少了因加载多个脚本导致的延时)。多个合并压缩成一个大的JS文件,并将其放在CDN中并引入也是可以的。
1.3无阻塞脚本
JS倾向于阻止浏览器的某些处理过程,如HTTP请求和用户界面更新,这是开发者所面临的最显著的性能问题。减少JS文件大小并限制HTTP请求数仅仅是创建响应迅速的Web应用的第一步。Web应用的功能越丰富,所需要的JS代码就越多,所以精简源代码不总是可行的。尽管下载单个较大的JS文件只产生一个HTTP请求,却会锁死浏览器一大段时间。为了避免这种情况,你需要向页面中逐步加载JS文件,这样做在某种程度上来说不会阻塞浏览器。
无阻塞脚本的秘诀在于,在页面加载完成后才加载JS代码。用专业术语来说,这意味着在window对象中的load事件触发后再下载脚本。有多种方式可以实现这一效果。
1.3.1延迟脚本
HTML4为<script>标签定义了一个扩展属性:defer。Defer属性指明本元素所含的脚本不会修改DOM,因此代码能安全地延迟执行。该属性只有IE4和FireFox3.5+的浏览器支持,所以它不是一个理性的跨浏览器解决方案。在其他浏览器中,defer属性会被直接忽略,因此<script>标签会以默认的方式处理(即会造成阻塞)。然而,如果你的目标浏览器支持的话,这仍然是个有用的解决方案。
带有defer属性的<script>标签可以放置在文档的任何位置。对应JS文件将页面解析到<script>标签时开始下载,但并不会执行,直到DOM加载完成(事件被触发前)。当一个带有defer属性的JS文件下载时,它不会阻塞浏览器的其他进程,因此这类文件可以与页面中的其他资源并行下载。
任何带有defer属性的<script>元素在DOM完成加载之前都不会被执行,无论内嵌或外链脚本都是如此。
例如:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Script Example</title> </head> <body> <script defer> alert("defer"); </script> <script> alert("script"); </script> <script> window.=function(){ alert("load"); } </script> </body></html>
这段代码在页面处理过程中会弹出三次提示框。不支持defer属性的浏览器的弹出属性是"defer"、"script"、"load"。而在支持defer属性的浏览器上,弹出的顺序是:"script"、"defer"、"load"。请注意,带有defer属性的<script>元素不是跟在第二个后面执行,而是在事件处理器执行之前被调用。
当然了,目前我在我自己电脑上执行了上述代码,基本都不支持defer,可能需要更低的版本才能支持。
1.3.2动态脚本元素
通过文档对象模型,你几乎可以用JS动态创建HTML中的所有内容。其根本在于,<script>标签与页面中的其他元素并无差异:都能通过DOM引用,都能在文档中移动、删除、甚至被创建。用标准的DOM方法可以非常容易地创建一个新的<script>元素。
13.3XMLHttpRequest脚本注入
另外一种无阻塞加载的脚本方法是使用XMLHttpRequest对象获取脚本并注入页面中。此技术会先创建一个XHR对象,然后用它下载JS文件,最后通过创建动态<script>元素将代码注入页面中。
var xmlhttp; if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari xmlhttp=new XMLHttpRequest(); } else {// code for IE6, IE5 xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.onreadystatechange=function() { if (xmlhttp.readyState==4 && xmlhttp.status==200) { document.getElementById("myDiv").innerHTML=xmlhttp.responseText; } } xmlhttp.open("GET","test.js",true); xmlhttp.send(); }
这段代码发送一个GET请求获取test.js文件。事件处理函数onReadyStateChange检查readyState是否为4,同时校验HTTP状态码是否有效(200表示有效响应,304意味着从缓存中读取)。
这种方法主要优点是:你可以下载JS代码但不立即执行。由于代码是在<script>标签之外返回的,因此它下载后不会自动执行,这使得你可以把脚本的执行推行到你准备好的时候。另一个优点是,同样的代码再所有的主流浏览器中无一例外都能正常工作。
这种方法的主要局限性是JS文件必须与所请求的页面处于相同的域,这意味着JS文件不能从CDN下载。因此大型Web应用通常不会采用XHR脚本注入。
1.3.4推荐的无阻塞模式
向页面中添加大量JS的推荐做法只需两步:先添加动态加载所需的代码,然后加载初始化页面所需的剩下的代码。因为第一部分的代码尽量精简,甚至可能只包含loadScript()函数,它下载执行都很快,所以不会对页面有太多影响。一旦初始代码就位,就用它来加载剩余的JS。
例如:
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="loader.js"></script><script>loadScript("the-rest.js",function(){ Application.init(); });
把这段代码加载放在</body>闭合标签之前。这样做有几个好处:
(1)确保JS执行过程中不会阻碍其他内容显示;
(2)当第二个JS文件完成下载时,应用所需的所有DOM结构已经创建完毕,并做好交互准备,从而避免了需要另一个事件(比如window.)来检测页面是否准备好。
小结:
管理浏览器中的JS代码是个棘手的问题,因为代码执行过程中会阻塞浏览器的其他进程,比如用户界面绘制。每次遇到<script>标签,页面都必须停下了等待代码下载(如果是外链文件)并执行,然后继续处理其他部分。尽管如此,还是有几种方法能减少JS对性能的影响:
(1)</body>闭合标签之前,将所有的<script>标签放到页面底部。这能确保在脚本执行前,页面已经完成渲染;
(2)合并脚本。页面中的<script>标签越少,加载也就越快,响应也就越迅速。无论是外链还是内嵌脚本都是如此;
(3)有多种无阻塞下载JS的方法:
a.使用<script>的defer属性(注意:高版本浏览器不支持);
b.动态创建<script>元素来下载并执行代码;
c.使用XHR对象下载JS代码并注入页面中;
通过以上策略,可以极大的提高那些需要使用大量JS的Web应用的实际性能。
我的感触:
全文本质其实这么几个?
1.JS脚本放置最底下(避免延迟导致渲染效果差);
2.合并代码,将大量JS合并和压缩为一个JS文件,本质上减少HTTP请求,同时也减少并行下载带来的延迟;
做到上述两点Web应用的性能也会得到很大程度上的提升,特别是做到2,2也正说明了webpack或者gulp流行的重要原因。
共同学习,写下你的评论
评论加载中...
作者其他优质文章