面对开发者的浏览器的Js性能可以说,最重要的是可用性问题。这个问题是复杂的,因为Js的阻塞特性,换句话说当Js代码正在被执行的时候,任何其他事情都不会发生。事实上,大多数浏览器使用单个进程来处理UI更新和Js执行,所以在任何时刻只有一个能发生。Js占用约多的时间来执行,浏览器能够响应用户操作之前等待的时间就越长。
在基础层面,意味着每一个<script>标签都会使页面等待脚本被解析和执行,不论Js代码是内联的还是外联的。页面下载和渲染必须停止并等待脚本完成执行,这个是页面声明周期中必要的部分,因为脚本执行过程中引起页面改变。典型的例子是在页面中使用document.write()
,比如:
<html>
<head>
<title>Script Example</title>
</head>
<body>
<p>
<script type="text/javascript">
document.write("The date is "+ (new Date()).toDateString());
</script>
</p>
</body>
</html>
当浏览器碰到<script>标签时,没有办法知道Js是否会在<p>标签内插入内容,因此浏览器停止处理页面,执行Js代码,然后继续解析和渲染页面。
脚本位置
传统的,<script>标签放在<head>标签内用于加载Js文件,<link>tags加载外部的css文件。比如:
<html>
<head>
<title>Script Example</title>
<-- Example of inefficient script positioning -->
<script type="text/javascript" src="file1.js"></script>
<script type="text/javascript" src="file2.js"></script>
<script type="text/javascript" src="file3.js"></script>
</head>
<body>
<p>Hello world!</p>
</body>
</html>
虽然这段代码看上去无害的,它实际上有一个严重的性能问题:有三个Js文件在<head>中被加载,因为每一个<script>标签阻塞页面继续渲染,直到脚本完全地被下载和执行。记住,在打开<body>标签之前,浏览器不会呈现任何页面内容。把Js放在页面头部导致了显著的延迟,通常用户还没有开始阅读或者与页面交互之前,就会出现空白的页面。为了懂得如何发生了,查看下每个资源下载的瀑布流图还是很有用的
现在浏览器都允许并行下载Js文件了,这是一个好消息,因为<script>标签不必阻塞其他<script>标签下载外部资源了。不幸的是,依然阻塞其他资源的下载,比如图片。即使下载Js不阻塞其他资源下载,页面还是必须等待Js代码下载和执行。问题还是没有解决。
因为脚本阻塞下载页面其他资源,所以推荐把所有的<script>标签尽可能放在<body>标签的底部,以不影响整个页面的下载,比如:
<html>
<head>
<title>Script Example</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<p>Hello world!</p>
<-- Example of recommanded script positioning -->
<script type="text/javascript" src="file1.js"></script>
<script type="text/javascript" src="file2.js"></script>
<script type="text/javascript" src="file3.js"></script>
</body>
</html>
合并脚本
因为每一个<script>标签在页面初始化下载的时候阻塞页面渲染,限制页面上<script>标签的数量是有帮助的。
每一个Http请求带来额外的性能开销,所以下载单个100KB的文件要比下载4个25KB的文件来得快。所以,现在外部Js文件的数量可以提升页面的性能。
典型的大网站或web应用程序会有多个Js文件请求。你可以通过合并这些文件为一个Js文件,然后使用一个<script>标签引入,来最小化性能影响。
比如,雅虎创建了用于分发Yahoo组合的处理程序,如何网站可以通过一个combo-handled URL拉取任何数量的YUI文件。比如:
<html>
<head>
<title>Script Example</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<p>Hello world!</p>
<-- Example of recommanded script positioning -->
<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.7.0/build/yahoo/yahoo-min.js&2.7.0/build/event/event-min.js"></script>
</body>
</html>
这个URL加载了2.7.0版本的yahoo-min.js和event-min.js文件。这些文件在服务器上分开存放,但是当URL请求的时候合并在一起返回。使用单个<script>标签来加载,而不是通过两个<script>标签。
非阻塞Script
保持Js文件小并且限制http请求数量只是对于小型网站的第一步。对于有这丰富功能的应用,有更多的Js代码请求,所以保持源代码小不总是一个选择。限制只下载单个大的Js文件将导致锁死浏览器一个较长的时间,尽管它只有一个HTTP请求。为了解决这种情况,你需要使用一种非阻塞浏览器的方式在页面中增量的添加更多的Js。
非阻塞脚本的秘密是在页面加载完成后加载Js。从技术层面讲,就是在window's load事件触发后下载Js代码,有几个技术可以做到。
Deferred Script
<script type="text/javascript" src="file1.js" defer></script>
一个带有defer的<script>标签可以放置在文档的任何位置,Js文件会在<script>标签被解析后开始下载,但是不会执行,知道DOM被完全加载(在onload事件之前执行)。当deferred脚本文件被下载的时候,它不阻塞浏览器,所以页面上的其他资源能并行下载。
动态脚本元素
Dom允许你使用js动态创建HTML文档的几乎任何部分,当然包括<script>标签。一个新的<script>标签可以使用标准的DOM方法来简单的创建:
var script = document.createElement("script");
script.type="text/javascript";
script.src="file1.js";
document.getElementsByTagName("head")[0].appendChild(script);
这个新的<script>元素加载源文件file1.js,一旦元素被添加到页面,文件就开始下载。关于这个技术最重要的是文件下载和执行不会阻塞页面其他进程,不管下载在哪儿初始化。你甚至能够放置你的代码在文档<head>中,也不会影响页面其他部分。
当使用动态脚本节点加载的文件下载完成,这代码就会立刻执行,当脚本是独立有效的,这会工作的很好。但可能脚本的执行依赖于页面上其他脚本,在这种情况下,你需要跟踪代码被完全下载并准备使用的状态,这是通过事件触发来完成的(脚本加载完成会触发load事件)。
var script=document.createElement("script");
script.type="text/javascript";
script.onload=function(){
alert("Script loaded!");
};
script.src="file1.js";
document.getElementsByTagName("head")[0].appendChild(script);
通常,你会考虑使用简单的方法去动态加载Js文件,下面是一个方法封装:
function loadScript(url,callback){
var script =document.createElement("script");
script.type="text/javascript";
if(script.readyState){
script.onreadystatechange=function(){
if(script.readyState=='loaded' || script.readyState=='complete'){
script.onreadystatechange=null;
callback();
}
}
}else{
script.onload=function(){
callback();
};
}
script.src=url;
document.getElementsByTagName("head")[0].appendChild(script);
}
你可能动态加载很多JS文件,但是你得考虑文件加载的顺序。主流的浏览器为确保脚本执行的顺序会按照你指定的来,其他浏览器下载和执行脚本的顺序由他们被服务器返回的先后顺序决定。你可以通过链式下载来控制顺序,比如:
loadScript("file1.js",function(){
loadScript("file2.js",function(){
loadScript("file3.js",function(){
alert("All files are loaded!");
})
})
})
XMLHttpRequest 脚本注入
另一个非阻塞检索脚本的方法是使用XMLHttpRequest对象,然后注入脚本到页面。
var xhr = new XMLHttpRequest();
xhr.open("get","file1.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);
这个方法主要的优势是你可以下载脚本代码,但不需要立刻执行它。因为代码不是通过<script>标签下载的,它不会一旦下载就立刻执行,允许你延迟它的执行,直到你已经准备好让它执行。