本文通过几个例子详述脚本对页面渲染的影响,以及浏览器正在加载提示 (标签页旋转按钮、页面停止渲染、光标停止响应)的行为。 介绍如何使用异步脚本载入策略提前 load 事件,提前结束浏览器的正在加载提示。

  • 脚本会阻塞 DOM 渲染,因此可以把不必要首屏载入的脚本异步载入。
  • 载入方式一:使用类似 requirejs 的方案,或在 load 事件后再插入外链脚本。
  • 载入方式二:XHR 获取内容后 Eval(不安全,且跨域不可用)。
  • 载入方式三:使用 <script>asyncdefer 属性。

DOM 渲染流程

要理解异步脚本载入的用处首先要了解浏览器渲染DOM的流程,以及各阶段用户体验的差别。 一般地,一个包含外部样式表文件和外部脚本文件的HTML载入和渲染过程是这样的:

  1. 浏览器下载HTML文件并开始解析DOM。
  2. 遇到样式表文件link[rel=stylesheet]时,将其加入资源文件下载队列,继续解析DOM。
  3. 遇到脚本文件时,暂停DOM解析并立即下载脚本文件。
  4. 下载结束后立即执行脚本,在脚本中可访问当前<script>以上的DOM。
  5. 脚本执行结束,继续解析DOM。
  6. 整个DOM解析完成,触发DOMContentLoaded事件。

脚本加载阻塞 DOM 渲染

众所周知,或者你自己实践当中,一定会发现这个问题,js下载缓慢的情况下,dom渲染被阻塞,体现的特别明显。例如现在很流行的react框架,就一个根dom节点,打包出来的js大的话,页面第一次渲染一片白,非常不友好!

1.异步加载脚本:XHR+Eval

我们知道XHR可以用来执行异步的网络请求,XHR Eval方法的原理便是通过XHR下载整个脚本,通过eval()函数来执行这个脚本。

1
$.get('/path/to/sth.js').done(eval);

因为XHR的下载过程是异步的,所以这个过程中浏览器图标不会显示『忙提示』。 JS的执行时间很短暂,可以认为页面始终不会停止响应。 XHR有跨域问题,因此该方法只适用于资源位于同一域名的情况(或者开启CORS响应头字段)。

因为eval()方法是不安全的,可以创建一个<script>标签,并把XHR获取的脚本注入进去。 再把 <script> 标签插入 DOM 它的内容就会执行。

2.异步加载脚本:Defer/Async

这是 HTML5 中标准的属性,用来在 HTML 标记中声名式地指定异步加载脚本。 除了 Opera 之外的浏览器基本都有支持。这个机制包括两个属性:defer和async。 例如:

1
2
<script src="one.js" async></script>     <!--异步执行-->
<script src="one.js" defer></script> <!--延迟执行-->

这两者有什么区别呢?请看下图(图片来自peter.sh):

  • 正常执行(无任何属性):在下载和执行脚本时HTML停止解析
  • 设置 defer:在下载脚本时HTML仍然在解析,HTML解析完成后再执行脚本。延迟执行不会阻塞渲染,额外的好处是脚本执行时页面已经渲染结束。
  • 设置 async:在下载脚本时 HTML 仍然在解析,下载完成后暂停HTML解析立即执行脚本。

3.AMD requirejs

这个办法超级好,前提是你打包的js 没有分块加载,也就是没有使用requirejs,否则,就会抱着个错误Mismatched anonymous define() module: function definition(name, global) [duplicate],特别坑。

特别是对于我这种懒人使用antd的ui框架,打包出来的js超级大, 又钱少买不起cdn,很多基于这个破东西的现成框架往往都已经引入了requirejs,这就导致我在模板文件里面,require(build.js)的时候出现上面的错误!!想了很多办法,都没解决掉这个重复声明变量的错,有解决掉的大神可以告诉我咋弄。

4. 手写异步加载:插入外链脚本标签

浏览器“载入中”的提示会让用户感觉网页慢!事实上我们应该关心的网页性能就是用户感受的性能。 这个“载入中”的提示消失的时机基本就是 load 事件发生的时机。所以问题就变成了如何提前 load 事件。
参考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function() {
function async_load1(){
.......
}
function async_load(){
var root_js = "${path}/dist/rechargev2/umi.8379c960.js",;
var _s = document.createElement('script'); // 可以嵌套很多层啊,加载完一个根js,再继续加载几个其他js,再继续加载js。。。。就可以控制页面的loading,不会让用户一直看白屏。
if (window.attachEvent){
_s.attachEvent('onload', async_load1);
} else{
_s.addEventListener('load', async_load1, false);
}
_s.type = 'text/javascript';
_s.defer = true;
// s.async = true;
_s.src = root_js;
var _x = document.getElementsByTagName('script')[0];
_x.parentNode.insertBefore(_s, _x);
}
if (window.attachEvent){
window.attachEvent('onload', async_load);
} else{
window.addEventListener('load', async_load, false);
}
})();

官方例子

1
2
3
4
5
6
7
8
9
<script>
var script = document.createElement("script");
script.addEventListener("load", function(event) {
console.log("Script finished loading and executing");
});
script.src = "http://example.com/example.js";
script.async = true;
document.getElementsByTagName("script")[0].parentNode.appendChild(script);
</script>

支持的标签

  • <body>
  • <frame>
  • <frameset>
  • <iframe>
  • <img>
  • <link>
  • <script>