可能很多人都知道JavaScript的载入和渲染会暂停DOM解析,但可能仍缺乏直观的体验。 本文通过几个例子详述脚本对页面渲染的影响,以及如何使用异步脚本载入策略提供页面性能和用户体验。 包括在脚本载入缓慢或错误时尽早显示整个页面内容,以及早点结束浏览器忙提示(进度条、旋转图标、状态栏等)。
要理解异步脚本载入的用处首先要了解浏览器渲染DOM的流程,以及各阶段用户体验的差别。 一般地,一个包含外部样式表文件和外部脚本文件的HTML载入和渲染过程是这样的:
link[rel=stylesheet]
时,将其加入资源文件下载队列,继续解析DOM。
以上的DOM。DOMContentLoaded
事件。上述步骤只是大致的描述,你可能还会关心下面两个问题:
之前的样式下载完成并渲染结束。详见 外部样式表与DOMContentLoaded事件延迟一文。脚本载入真的会暂停DOM渲染吗?非常真切。 比如下面的HTML中,在脚本后面还有一个 标签。
Hello
World!
我们编写服务器端代码(见本文最后一章),让 /will-not-stop-loading.js
始终处于等待状态。 此时页面的显示效果:
脚本等待下载完成的过程中,后面的 World
不会显示出来。直到该脚本载入完成或超时。 试想如果你在 中有这样一个下载缓慢的脚本,整个
都不会显示, 势必会造成空白页面持续相当长的时间。 所以 较好的实践方式是将脚本放在
尾部。
很多被墙的网站加载及其缓慢就是因为DOM主体前有脚本被挡在墙外了。
既然脚本载入会暂停DOM渲染,OK我们把脚本都放在 尾部。 这时页面可以被显示出来了, 但是在脚本载入前,
DOMContentLoaded
事件仍然不会触发。 请看:
Hello
World!
这时 Wrold!
会显示,但浏览器忙指示器仍在旋转。 这是因为 DOM 仍然没有解析完成,毕竟最后一个 标签还未获取到嘛! 当然
DOMContentLoaded
事件也就不会触发。 DOM loaded!
对话框也不会弹出来。
直到超时错误发生, DOMContentLoaded
才会触发(在我的Chrome里超时用了好几分钟!), 此时对话框也会弹出:
本文关心的核心问题是页面性能和用户体验,现在来考虑一个问题:
对于非必须的页面脚本,在它的载入过程中如何取消浏览器的忙提示。
首先想到的办法一定是从HTML中干掉那些 ,然后在JavaScript中动态插入
标签。 比如:
var s = document.createElement('script');
s.src = "/will-not-stop-loading.js";
document.body.appendChild(s);
不贴图了,标签页上的图标确实在旋转,和上一小节中的图一样 :(
那么等 DOMContentLoaded
会后再来插入呢?
document.addEventListener('DOMContentLoaded', function(){
var s = document.createElement('script');
s.src = "/will-not-stop-loading.js";
document.body.appendChild(s);
});
上述代码仍然无法阻止浏览器忙提示。这充分说明浏览器JavaScript执行是单线程的,DOM事件机制也不例外。
为了阻止浏览器忙提示,应当可以使用异步加载脚本的策略。先看一个简单的示例:
setTimeout(function(){
var s = document.createElement('script');
s.src = "/will-not-stop-loading.js";
document.body.appendChild(s);
});
setTimeout
未指定第二个参数(延迟时间),会立即执行第一个参数传入的函数。 但是JavaScript引擎会将该函数插入到执行队列的末尾。 这意味着正在进行的DOM渲染过程完全结束后(此时浏览器忙提示当然会消失),才会调用上述函数。 看图:
其中 /will-not-stop-loading.js
仍处于 pending
状态,但浏览器忙提示已经消失。 然而在Chrome中,如果插入 时仍有其他资源正在载入,那么上述做法仍然达不到效果 (浏览器会判别为页面仍未完全载入)。 总之: 异步加载脚本来禁止浏览器忙提示的关键在于让DOM先加载完毕。
不要沮丧,在实际的项目中有两种成熟的办法可以禁止浏览器忙提示。
使用AJAX获取脚本内容,并用Eval来运行它。 因为AJAX一般不会触发浏览器忙提示,脚本执行只可能让浏览器暂停响应也不会触发忙提示。
首先在需要异步加载的脚本设置 type="text/defered-script"
,并用 data-src
代替 src
防止浏览器直接去获取: