用 asyncJS 异步加载 JavaScript

20 Oct 2013

根据 MDN 的文档和实际的测试,如果有 <script> 标签在 <link rel="stylesheet" ...> 之后,页面要等待 CSS 加载才能完成解析。造成 DOMContentLoaded 的延迟。

真实的页面可能更糟,在页面底部有外链的 JavaScript,之后有内嵌的函数。在这种情况下,页面需要等待 JavaScript 加载完成之后才能执行后续的函数,造成 DOMContentLoaded 的进一步延迟。

美团网现在的页面也类似这样的结构,文件头是外链的 CSS 文件,首屏加载不到十张左右的图片,文件末尾加载 YUI,在 YUI 加载之后有一些内嵌的 JavaScript ,用来加载更多资源。

可以想象,DOMContentLoaded 时间还有提高的潜力。

理想情况

在理想情况下,<link rel="stylesheet" ...> 之后就不要有 <script> 了,无论是外链还是内嵌。这样一来 DOMContentLoaded 可以在解析完页面之后就触发,网页的「白屏时间」会更短,用户的访问体验也更好。

可是需要动态加载的内容怎么办?把外链 JavaScript 全放到 CSS 的前面一样会造成阻塞。总不能不用 JavaScript 吧?

来个速效对策?

第一个想到的办法是在 DOMContentLoaded 触发时候才动态加入所有的 JavaScript。这个办法在现实中使用有两个问题:

按照刚才的解决思路,在 CSS 之后尽量避免出现<script> ,如果一定要用,那至少去掉所有外链资源,异步加载所有的 JavaScript,这样解析起来造成的阻塞会小很多。

在优化前,需要等待在文档接收完成,浏览器才会开始下载底部的 JavaScript ,等这些都下载完成之后才会触发 DOMContentLoaded。优化之后,一旦文档接收完成、CSS 加载完成,就可以 DOMContentLoaded 了。

更好的是,这样的优化并没有延迟 JavaScript 的下载;外链的资源开始下载的时间反而提前了,在浏览器接收到 <head> 的时候开始下载额外的两个外链资源,这样 load 时间也可能提前,这对首次加载的效果更加显著。

async script:看起来还不错

<script async src="..."> ,异步加载 JavaScript ,加载完成之后自动执行。

这对现代浏览器而言都不在话下,老的浏览器也能(使用同步加载)兼容这个写法。看起来我们似乎解决了问题。

但这破坏了页面上脚本的顺序执行:async 标签后的<script>在执行的时候,前面 async 的内容可能还没加载完。那内嵌的函数该怎么执行?

<script async src="..." onload="function() {...}">

资源加载完成后触发 onload/onreadystatechange,我们似乎又一次解决了问题:加载完成后执行内嵌函数;所有的浏览器都支持,看起来可以按时下班了。

可扩展性?

现代的网页依赖关系可没这么简单。那么模块的依赖怎么处理?

我们来看一个最简单的例子,我们需要用 Highcharts 在页面上画一个图。但是 Highcharts 依赖 jQuery,所以我们这么写:

<script src="jquery.js"></script>
<script>
// 使用 jQuery
...
</script>
<script src="highcharts.js"></script>
<script>
// 使用 jQuery 和 Highcharts
...
</script>

<script async> 的方式,怎么写?

我们首先写上

<script async src="jquery.js" onload="function() {...}">

在 onload 函数里执行第一个内嵌函数,再创建一个异步加载 Highcharts 的 <script> 元素,在这个新元素的 onload 里写下第二个内嵌函数。

这样写起来非常啰嗦,对稍微复杂一些依赖关系,几乎没法按照这种方法来实现异步加载。

介绍 asyncJS

<script async> 加载的思路,我写了 asyncJS 。使用它来管理 JavaScript 加载可以大幅提高 DOMContentLoaded 的触发,获得更快的页面响应时间。

它可以加载外部资源

asyncJS("jquery.js")

可以执行函数

asyncJS(function() {...});

也可以执行字符串形式的 script

asyncJS('alert("This is awesome");')

还能处理外部脚本的依赖,加载完之后执行回调函数

var q = asyncJS();
q.add("jquery.js");
q.whenDone(function() {
    // 使用 jQuery
});

// HighCharts 会等待 jQuery 加载完成
q.then("highcharts.js");
q.whenDone(function() {
    // 使用 jQuery 和 Highcharts
});

可以调用的方法有三个:add 添加互不依赖的任务,then 添加依赖的任务。whenDone 指定一个回调函数,在之前的任务完成后执行。

更详细的解释请看文档

asyncJS 使用范例

下载代码后查看 examples 目录的范例和对比。

使用 asyncJS 意味着...

因为所有的 JavaScript 都是异步加载的,页面 <script> 不一定是顺序执行的。你不能保证假定内嵌函数执行时,之前的外链资源已经下载解析完成,依赖外链资源的函数必须使用 asyncJS 管理执行。这要求开发人员对页面 JavaScript 的依赖有一定了解,比如什么操作需要等待哪些模块等,就像使用 YUI().use 一样。

使用 asyncJS 会带来了明显好处:提前了资源下载、加快了页面解析,但也会会增加一点页面的代码量。和它带来的好处相比,我认为是值得的。

不是有 YUI().use 吗?

YUI().use 是一个更复杂的依赖管理器,它的缺点就是需要依赖 YUI。为了使用依赖管理器,你必须等到 YUI 加载完成。比方说你想加载一个很小的模块,那也要等到 300KB 的 YUI 加载完成之后才能开始安排,这样局限性太大了。

asyncJS 设计目标是做一个不依赖外部库的启动(bootstrap)管理器,主要作用是优化页面最初加载过程。它在 gzip 压缩后只有 1.3kB,可以很方便的内嵌到 <head> 里。asyncJS 和 YUI().use 并不冲突,还可以提早YUI 等基础库的加载。

改进 asyncJS

asyncJS 目前没有对异步操作进行处理,如果添加一个异步任务(比如 AJAX 请求),`whenDone` 在函数体完之后就立刻执行,而没有等到异步操作拿回数据。

asyncJS 0.7+ 已经可以正确处理异步函数调用了。

欢迎参与 asyncJS 项目,有任何意见和 Bug 都何以在 Github 上提给我。

comments powered by Disqus