高性能网站优化-无阻塞加载脚本
《高性能网站建设进阶指南》
按理来说,JavaScript在浏览器中的性能问题是开发人员面对的最重要的可用性问题。但由于JavaScript天生的阻塞性质,问题变得复杂。大多数浏览器在下载或者执行脚本的同时不会下载其他内容,使用单线程处理JavaScript的解析和UI的更新。当遇到这种情况时,希望以不阻塞其他内容下载的方式来加载JavaScript。有些技术可以做到这点,使页面加载更快。
头疼的脚本阻塞并行下载
在介绍JavaScript加载优化技术之前,先看看浏览器默认的方式。<script> 标签可以放在 <head> 或者 <body> 里面的任意位置。一般来说都将 <script> 和 <link> 标签一起放在 <head> 中,这样一来页面加载的时候会先加载它们,页面的样式和行为看起来正常。
<html>
<head>
<meta charset="UTF-8">
<title>Demo</title>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/base2.css">
<script src="scripts/file1.js"></script>
<script src="scripts/file2.js"></script>
<script src="scripts/file3.js"></script>
</head>
<body>
<p>Hello,CrispElte</p>
</body>
</html>
尽管这端代码开始来无伤大雅,但实际上存在几个性能问题:有三个脚本文件,每个文件在下载和执行时候都会阻塞页面的加载。只有脚本执行完成之后,才会继续加载其他资源,比如,图片,CSS文件,iframe等。
我们可以将 <script> 标签放在页面的底部,即 </body>标签之前。尽管脚本文件的加载会阻塞页面,但是页面在脚本呢加载之前已经加载完成,所以不用担心阻塞。
脚本应该顺序执行,但是没有必要顺序下载。IE8第一次实现了脚本的并行下载,但是在脚本下载并执行完毕之前依旧阻塞了后面的资源。
最终的目的时让脚本与其他资源并行下载,并且希望兼容所有的浏览器。
让脚本运行得更美好
有几种动态加载外部脚本的技术可以使页面不会被脚本的阻塞行为所影响。
XHR Eval
通过XMLHttpRequest从服务端获取脚本,然后通过eval命令执行内容
var xhrObj=new XMLHttpRequest();
xhrObj.onreadystatechange=function(){
if(xhrObj.readyState == 4 && xhrObj.status == 200){
eval(xhrObj.responseText);
}
}
xhrObj.open("GET","file1.js",true);
xhrObj.send(null);
这个方法的主要缺陷是,动态加载的脚本必须是同域的。
XHR注入
与XHR Eval类似,同样是通过 XMLHttpRequest 对象来获取脚本,不同之处在于,这个方法时不是用 eval 而是创建一个 script 的 DOM 元素,然后将 XMLHttpRequest 的响应写入script标签中来执行JavaScript。
var xhrObj=new XMLHttpRequest();
xhrObj.onreadystatechange=function(){
if(xhrObj.readyState == 4 && xhrObj.status == 200){
var script=document.createElement("script");
document.getElementsByTagName("head")[0].appendChild(script);
script.text=xhrObj.responseText;
}
}
xhrObj.open("GET","file1.js",true);
xhrObj.send(null);
和 XHR Eval 方法一样,加载的脚本必须是同域的。
Script in Iframe
在页面中 iframe 与其他资源是并行下载的,可以利用iframe无阻塞加载JavaScript。
由于 iframe 认为返回的是 HTML 文档,所以将src设置成一个 HTML 文件而不是 js 文件。
而我们要做的就是在 HTML 文档中将外部脚本转换成行内脚本。
与 XHR Eval 和 XHR 注入这两种方法类似,这个方法要求 iframe URL 和主页同域。满足同域要求之后,我们需要修改 JavaScript 来创建他们之间的关联,其本质就是获得引用 iframe 的 JavaScript 标示符。
//使用 "iframes" 中访问主页上的 iframe
window.frames[0].somefunction();
//使用 "getElementById" 访问主页上的iframe
document.getElementById("myIframe").contentWindow.someFunction();
可以在iframe中使用parent变量引用父页面
function changeBg(){
var body=parent.document.body;
body.className="red";
}
概括一下:主页面中添加一个 iframe 标签,其 src 指向一个 HTML 文档,在这个 HTML 文档中编写行内 JavaScript 代码,也可以引用外部的 JavaScript 文件
Script DOM Element
创建一个script标签并设置其src,代码很简单。
var script=document.createElement("script");
script.src="demo2.js";
document.getElementsByTagName("head")[0].appendCHild(script);
下载过程中用这种方式创建脚本不会阻塞其他的资源,同时这种方法允许跨域。
defer还是async
在最新的标准中,script标签定义了defer属性和async属性,都是让脚本并行下载,但是defer下脚本按照顺序执行,而async不按顺序执行脚本。
document.write Script Tag
最后一种技术是使用 document.write 把 HTML 标签 script 写入页面中。这种技术只在IE中是并行加载脚本的。虽然多个脚本可以并行下载,但在下载脚本时,浏览器仍然阻塞其他类型的资源。