1. 1. 顺序执行、并发加载
  2. 2. 阻塞
    1. 2.1. CSS阻塞
    2. 2.2. JS阻塞
  3. 3. 引入方法
    1. 3.1. 脚本的位置
    2. 3.2. 无阻塞脚本
    3. 3.3. 动态脚本
    4. 3.4. XMLHttpRequest(XHR)对象
页面加载——浏览器渲染

一个网站在浏览器端是如何进行渲染的呢?

  • 根据HTML结构生成DOM tree
  • 根据CSS生成CSSOM
  • DOMCSSOM整合形成RenderTree
  • 根据RenderTree开始渲染和展示
  • 遇到<script>时,会执行并阻塞渲染

顺序执行、并发加载

因为解析过程是一个从上到下的过程,所以渲染过程是顺序执行的。而所谓的并发加载指的是当浏览器引入<link><script>,多个标签的资源可以并发加载。但是并发度是受浏览器自身能力限制的。

对于<img>所载入的图片,是异步请求的,并不会阻塞页面的渲染,图片的加载速度受其本身大小的影响(图片太大可能在整个页面加载完之后它还没加载出来)。

1
2
3
4
5
6
7
window.addEventListener('load', function() {
// 页面的全部资源加载完才会执行,包括图片、视频
})

document.addEventListener('DOMContentLoaded', function() {
// DOM 渲染完即可执行,此时图片、视频可能还没加载完
})

阻塞

CSS阻塞

  • css在<head>中阻塞页面的渲染:即这个页面要呈现出效果需要等待这个<link>所对应的css资源加载完成以后才能进行渲染。如果css并不是在<head>中引入的话,会出现元素先展示在页面上过一会样式才呈现的情况,所以推荐css在<head>标签中就引入。
  • css阻塞js的执行:即在css资源加载完成之前,后续的js的是无法执行的。由于js文件经常会操作DOM元素,而操作过程中可能涉及css样式的修改,它的修改是依赖之前引入的css所具有的样式的,所以css会阻塞js的执行。
  • css不阻塞外部脚本的加载:即css资源不会阻塞后续的js资源的加载,但是只能加载不会执行。由于webkit存在HTMLPreloadScanner类,是一个预先扫描器,它可以预先扫描后面的词语,在扫描到一些需要加载的资源后,会通过预资源加载器请求后续的资源加载。

JS阻塞

  • 直接引入的js阻塞页面的渲染:直接引入指的是没有通过deferasync方法直接用<script>引入的js资源。如果在标签中指定了defer方法,这个资源将在页面解析到<script>的时候就开始下载,但不会执行,直到DOM加载完成(触发onload事件前)才会被调用。而asyncdefer的作用是相同的,它们的区别在于sync的执行是在下载完成之后就开始执行了,所以进行异步加载的脚本最好不要操作DOM元素。
  • js不阻塞资源的加载:与css的加载同理,由于有扫描器的存在,资源会并行加载。
  • js顺序执行,阻塞后续js逻辑的运行:即js的执行顺序是和引入的顺序一致。这是由于js的执行是单线程的,所以它会顺序执行并阻塞后续js的执行。

引入方法

脚本的位置

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<title>Source Example</title>
<script type="text/javascript" src="script1.js"></script>
<script type="text/javascript" src="script2.js"></script>
<script type="text/javascript" src="script3.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<p>Hello world!</p>
</body>
</html>

当我们在<head>中引入js文件时,由于js的阻塞特性,当浏览器解析到<script>标签时,浏览器会停止解析其后的内容,而优先下载脚本文件,并执行其中的代码。虽然后续的资源仍然可以继续加载,却无法渲染,这就造成了打开页面后的空白时间过长,影响用户体验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<title>Source Example</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<p>Hello world!</p>

<!-- <script> 文件推荐放在这儿 -->
<script type="text/javascript" src="script1.js"></script>
<script type="text/javascript" src="script2.js"></script>
<script type="text/javascript" src="script3.js"></script>
</body>
</html>

所以建议把<script>放在<body>末尾,因为此时样式和DOM元素都已经加载并渲染完毕,所以页面的下载就不会显得太慢。

无阻塞脚本

  • defer属性:是HTML4为<script>拓展的属性,指明本元素所含的脚本不会修改DOM,因此代码能安全地延迟执行。只支持IE4和Firefox 3.5以上版本的浏览器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<title>Script Defer Example</title>
</head>
<body>
<script type="text/javascript" defer>
console.log("defer");
</script>
<script type="text/javascript">
console.log("script");
</script>
<script type="text/javascript">
window.onload = function(){
console.log("load");
};
</script>
</body>
</html>

该段代码执行后的结果是scriptdeferload,表明含有defer属性的脚本是在onload执行前被调用的,不论它写的位置在哪里。该方法虽好却存在兼容性问题。

  • async属性:是HTML5为<script>拓展的属性,作用和defer一样,能够异步地加载和执行脚本。它比defer有更好的兼容性,但由于async在加载完毕后就会立即执行,所以脚本的执行顺序就可能不是按照html文本的引入顺序,如果两个js前后有依赖关系,就会出现错误。

动态脚本

1
2
3
4
5
var script = document.createElement ("script");

script.type = "text/javascript";
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);

该方式可以让<script>无论在什么地方引入,文件的下载和运行都不会阻塞页面的处理过程。但是由于引入的文件的下载和运行和其他DOM元素是并行的,所以可能出现这个文件中绑定操作的DOM元素还没加载,因为找不到而报错。

1
2
3
4
5
6
7
8
var script = document.createElement ("script");

script.type = "text/javascript";
script.onload = function(){
console.log("Script loaded!");
};
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);

在Firefox、Opera、Chrom和Safari 3+中提供了script.onload事件,可以监听onload事件来加载js脚本。而对于IE,则是用另一种方式,即readystatechange事件。

uninitialized:默认状态
loading:下载开始
loaded:下载完成
interactive:下载完成但尚不可用
complete:所有数据已经准备好

1
2
3
4
5
6
7
8
9
10
11
12
var script = document.createElement("script");
script.type = "text/javascript";

//Internet Explorer
script.onreadystatechange = function(){
if (script.readyState == "loaded" || script.readyState == "complete"){
script.onreadystatechange = null;
console.log("Script loaded.");
}
};
script.src = "script1.js";
document.getElementsByTagName("head")[0].appendChild(script);

虽然以上的动态加载方法可以解决阻塞问题,却还是存在依赖问题,即无法控制两个互相依赖的js文件的先后加载顺序,所以我们可以对这个方法进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function loadScript(url, callback) {
var script = document.creatElement("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);
}

如此就可以通过嵌套调用来保证他们的加载顺序:

1
2
3
4
5
loadScript("script1.js",function() {
loadScript("script2.js",function() {
alert("all files are loaded!");
})
})

XMLHttpRequest(XHR)对象

可以利用ajax异步请求的方式,向服务器发送一个获取js文件的请求,在请求成功之后执行动态加载的方法。该方法的优点是可以下载但不立即执行js代码,并且兼容性好。但此方法也有限制:即js文件必须与页面放置在同一个域内(跨域问题),不能从CDN下载,所以大型网站通常不采用XHR脚本注入技术。