主要弄清楚几个概念:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy execution),async 属性, defer 属性。
javascript的同步,异步,延迟加载
加载的网络 timeline 是瀑布模型,而异步加载的 timeline 是并发模型。
同步加载
同步模式,又称阻塞模式。javascript在默认情况下是会阻塞加载的。当前面的javascript请求没有处理和执行完时,会阻止浏览器的后续处理,如文件加载(图像),DOM tree的渲染,代码的执行。
js之所以要同步执行,是因为 js 中可能有输出 document 内容、修改dom、重定向等行为,所以默认同步执行才是安全的。
以前的一般建议是把script放在页面末尾之前,这样尽可能减少这种阻塞行为,而先让页面展示出来。
异步加载
异步加载又叫非阻塞,浏览器在下载执行 js 同时,还会继续进行后续页面的处理。
有很多种方式可以实现异步加载。下面是其中的几种。
延迟加载
延迟加载:有些 js 代码并不是页面初始化的时候就立刻需要的,而稍后的某些情况才需要的。延迟加载就是一开始并不加载这些暂时不用的js,而是在需要的时候或稍后再通过js 的控制来异步加载。
也就是将 js 切分成许多模块,页面初始化时只加载需要立即执行的 js ,然后其它 js 的加载延迟到第一次需要用到的时候再加载。
特别是页面有大量不同的模块组成,很多可能暂时不用或根本就没用到。
就像图片的延迟加载,在图片出现在可视区域内时(在滚动条下拉)才加载显示图片。
异步加载的几种方法
Script DOM Element方法
1 | (function() { |
这种方法是在页面中script标签内,用 js 创建一个 script 元素并插入到 document 中。这样就做到了非阻塞的下载 js 代码。将js代码包裹在匿名函数中并立即执行的方式是为了保护变量名泄露到外部可见,这是很常见的方式,尤其是在 js 库中被普遍使用。我的博客就用了百度统计,里面脚本也是通过这种方式。1
2
3
4
5
6
7
8
9<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "******";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
但是,这种加载方式在加载执行完之前会阻止 onload 事件的触发,而现在很多页面的代码都在 onload 时还要执行额外的渲染工作等,所以还是会阻塞部分页面的初始化处理。
onload时的异步加载
1 | (function() { |
这和前面的方式差不多,但关键是它不是立即开始异步加载 js ,而是在 onload 时才开始异步加载。这样就解决了阻塞 onload 事件触发的问题。这种方法也是google最推荐的方法。因为它完全解决在web页面完全加载后,再加载外部js的问题。
defer和async
在看这两个的作用和区别之前,一定要先理解下面内容。
JS的加载其实是由两阶段组成:下载内容(download bytes)和执行(parse and execute)。我们都知道通过网络下载 script 需要明显的时间,但容易忽略了第二阶段,解析和执行也是需要时间的。script的解析和执行所花的时间比我们想象的要多,尤其是script 很多很大的时候。有些是需要立刻执行,而有些则不需要(比如只是在展示某个界面或执行某个操作时才需要)。
这两种方法都可以实现异步加载,那么有什么区别呢?
下面来看一张图:(非常感谢(sf的这篇文章)[https://segmentfault.com/q/1010000000640869])
蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。
第一种,没有 defer 或 async,按正常情况加载。1
<script src="script.js"></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。这种也就是为什么我们会出现白页的原因。
第二种,有 async1
<script async src="script.js"></script>
有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。async属性是HTML5新增的。作用和defer类似,但是它将在下载后尽快执行,不能保证脚本会按顺序执行。它们将在onload 事件之前完成。
第三种,有defer1
<script defer src="myscript.js"></script>
加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。
这时候就回想,这个onload和DOMContentLoaded到底什么区别呢?补充:DOMContentLoaded 与 OnLoad 事件
DOMContentLoaded : 页面(document)已经解析完成,页面中的dom元素已经可用。但是页面中引用的图片、subframe可能还没有加载完。
OnLoad:页面的所有资源都加载完毕(包括图片)。浏览器的载入进度在这时才停止。
这两个时间点将页面加载的timeline分成了三个阶段
这张图告诉了我们几件事情:
- defer和async的异步是相对于DOM TREE的解析。它们在网络读取没有任何差别。差别在于下载完了脚本后,何时执行。
- defer是全部元素解析完成之后(DOMContentLoaded)执行。所以是按照加载顺序时执行脚本。而async对它来说脚本的加载和执行是紧紧挨着的。,所以不管你声明的顺序如何,只要它加载完了就会立刻执行。所以可能会出现乱序。
- async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics和baidu统计。
延迟加载的方法
1 | window.onload = function() { |