异步脚本载入提高页面性能

标签: AJAX Chrome DOM HTML JavaScript | 发表时间:2016-05-18 00:00 | 作者:Harttle
出处:http://harttle.com/

可能很多人都知道JavaScript的载入和渲染会暂停DOM解析,但可能仍缺乏直观的体验。 本文通过几个例子详述脚本对页面渲染的影响,以及如何使用异步脚本载入策略提供页面性能和用户体验。 包括在脚本载入缓慢或错误时尽早显示整个页面内容,以及早点结束浏览器忙提示(进度条、旋转图标、状态栏等)。

DOM 渲染流程

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

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

上述步骤只是大致的描述,你可能还会关心下面两个问题:

  • 资源文件下载队列。样式表、图片等资源文件的下载不会暂停DOM解析。浏览器会并行地下载这些文件,但通常会限制并发下载数,一般为3-5个。可以在开发者工具的Network标签页中看到。
  • 执行脚本文件前,浏览器可能会等待该 <script>之前的样式下载完成并渲染结束。详见 外部样式表与DOMContentLoaded事件延迟一文。

脚本载入暂停DOM渲染

脚本载入真的会暂停DOM渲染吗?非常真切。 比如下面的HTML中,在脚本后面还有一个 <h1>标签。

   <!DOCTYPE html>
<html>
<body>
  <h1>Hello</h1>
  <script src="/will-not-stop-loading.js"></script> 
  <h1>World!</h1>
</body>
</html>

我们编写服务器端代码(见本文最后一章),让 /will-not-stop-loading.js始终处于等待状态。 此时页面的显示效果:

js block dom render

脚本等待下载完成的过程中,后面的 World不会显示出来。直到该脚本载入完成或超时。 试想如果你在 <head>中有这样一个下载缓慢的脚本,整个 <body>都不会显示, 势必会造成空白页面持续相当长的时间。 所以 较好的实践方式是将脚本放在 <body>尾部。

很多被墙的网站加载及其缓慢就是因为DOM主体前有脚本被挡在墙外了。

DOMContentLoaded 延迟

既然脚本载入会暂停DOM渲染,OK我们把脚本都放在 <body>尾部。 这时页面可以被显示出来了, 但是在脚本载入前, DOMContentLoaded事件仍然不会触发。 请看:

   <!DOCTYPE html>
<html>
<body>
  <h1>Hello</h1>
  <h1>World!</h1>
  <script>
    document.addEventListener('DOMContentLoaded', function(){
      alert('DOM loaded!');
    });
  </script>
  <script src="/will-not-stop-loading.js"></script> 
</body>
</html>

这时 Wrold!会显示,但浏览器忙指示器仍在旋转。 这是因为 DOM 仍然没有解析完成,毕竟最后一个 <script>标签还未获取到嘛! 当然 DOMContentLoaded事件也就不会触发。 DOM loaded!对话框也不会弹出来。

dom not loaded with script pending

直到超时错误发生, DOMContentLoaded才会触发(在我的Chrome里超时用了好几分钟!), 此时对话框也会弹出:

dom-loaded-as-script-timeout

浏览器忙提示

本文关心的核心问题是页面性能和用户体验,现在来考虑一个问题:

对于非必须的页面脚本,在它的载入过程中如何取消浏览器的忙提示。

首先想到的办法一定是从HTML中干掉那些 <script>,然后在JavaScript中动态插入 <script>标签。 比如:

   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渲染过程完全结束后(此时浏览器忙提示当然会消失),才会调用上述函数。 看图:

async script loading

其中 /will-not-stop-loading.js仍处于 pending状态,但浏览器忙提示已经消失。 然而在Chrome中,如果插入 <script>时仍有其他资源正在载入,那么上述做法仍然达不到效果 (浏览器会判别为页面仍未完全载入)。 总之: 异步加载脚本来禁止浏览器忙提示的关键在于让DOM先加载完毕

最佳实践

不要沮丧,在实际的项目中有两种成熟的办法可以禁止浏览器忙提示。

AJAX + Eval

使用AJAX获取脚本内容,并用Eval来运行它。 因为AJAX一般不会触发浏览器忙提示,脚本执行只可能让浏览器暂停响应也不会触发忙提示。

首先在需要异步加载的脚本设置 type="text/defered-script",并用 data-src代替 src防止浏览器直接去获取:

   <script type="text/async-script" data-src="http://foo.com/bar.js">

然后在站点的公共代码中加入『异步脚本加载器』:

   $('[type="text/defered-script"]').each(function(idx, el){
    $.get(el.dataset.src, eval);
});

注意:使用AJAX GET脚本文件时不要设置 Content-Type: "application/javascript" (包括 jQuery.getScript)。 这会使浏览器发现你是在加载脚本,进而触发忙提示指示器。 当然,如果此时页面已然载入完毕,任何AJAX都不会触发忙提示了。

上述方法的缺点在于,一旦被引入的JavaScript中需要以相对路径的方式载入其他JavaScript就会引发错误。 因为被Eval的脚本中,当前路径变成了页面所在路径,不再是原来的 <script>src所指的路径。 这在使用第三方库时非常常见。

Load 事件

既然禁止浏览器忙指示器的关键在于让DOM加载完毕,那就绑定页面载入完毕的事件: load。 例如:

   $(window).load(function(){
    $('script[type="text/async-script"]').each(function(idx, el){
        var $script = $('<script>');
        if(el.dataset.src) $script.attr('src', el.dataset.src);
        else $script.html(el.text);
        $script.appendTo('body');
        el.remove();
    });
});
  • 对于外部 <script>,生成一个新的包含正确 src<script>
  • 对于行内 <script>,生成一个新的包含正确内容的 <script>type默认即为 "application/javascript"

该方法采用DOM中 <script>加载的方式,没有AJAX+Eval改变脚本中当前路径的缺点。 http://harttleland.com中的Google Analytics、MathJax等脚本都采用这种处理方式。

服务器工具

本文所做实验服务器端都使用Node.js写成:

   const http = require("http");
const fs = require('fs');
const port = 4001;

var server = http.createServer(function(req, res) {
    switch (req.url) {
        case '/':
            var html = fs.readFileSync('./index.html', 'utf8');
            res.setHeader("Content-Type", "text/html");
            res.end(html);
            break;
        case '/will-not-stop-loading.js':
            break;
    }
});

server.listen(port, e =>
    console.log(`listening to port: ${port}`));

参考阅读

相关 [异步 脚本 性能] 推荐:

异步脚本载入提高页面性能

- - Harttle Land
可能很多人都知道JavaScript的载入和渲染会暂停DOM解析,但可能仍缺乏直观的体验. 本文通过几个例子详述脚本对页面渲染的影响,以及如何使用异步脚本载入策略提供页面性能和用户体验. 包括在脚本载入缓慢或错误时尽早显示整个页面内容,以及早点结束浏览器忙提示(进度条、旋转图标、状态栏等). 要理解异步脚本载入的用处首先要了解浏览器渲染DOM的流程,以及各阶段用户体验的差别.

mysql性能优化脚本mysqltuner.pl

- - 开心平淡对待每一天。热爱生活
 无意中发现了,major哥们开发的一个性能分析脚本,很有意思,可以通过这个脚本学学他的思想. 官方站点: http://blog.mysqltuner.com/get-involved/.

生成MySQL性能数据分析报告的Shell脚本

- EricSheng - MySQLOPS 数据库与运维自动化技术分享
【导读】 MySQL开源产品有非常丰富的分享出来可用的资源,比如本文要介绍的内容:如何每天简单地获得MySQL性能数据的报告,我们就可以利用perl脚本工具mysqlreport生成详细的性能数据分析报告,再借助sendEmail工具以邮件的方式发送出来,从而提高我们的工作效率,上期给大家介绍了SHELL脚本Get_Local_Kpi对我们工作的帮助.

Jmeter性能测试脚本录制和监控

- - 互联网 - ITeye博客
针对接口的压测,借用SoapUI测试工具录制脚本:. 1、被测接口remindFacade下的getRemindById接口. 步骤:打开jmeter----测试计划下建线程组----线程组下加HTTP请求(下面加断言)----线程组下面加监听器(聚合及查看结果树),基本架子已经ok. 2、ip你需要访问的服务器地址,http协议和post的请求方法,路径是被测接口的路径.

Android性能优化之实现双缓存的图片异步加载工具(LruCache+SoftReference) - 拿来即用

- - CSDN博客推荐文章
之前在郭大神的博客看到使用LruCache算法实现图片缓存的.这里仿效他的思路,自己也写了一个. 并加入ConcurrentHashMap<String, SoftReference<Bitmap>>去实现二级缓存,因为ConcurrentHashMap是多个锁的线程安全,支持高并发.很适合这种频繁访问读取内存的操作..

mysql backup 脚本

- - ITeye博客
网上备份脚本很多,但考虑都不周全. 保证创建备份文件只能是创建者跟root可以访问,其他用户没有权限,保证了数据库备份的安全. 上面脚本是负责备份的份数管理,. 已有 0 人发表留言,猛击->> 这里<<-参与讨论. —软件人才免语言低担保 赴美带薪读研.

RMAN 备份脚本

- - CSDN博客数据库推荐文章
RMAN冷备份、一致性备份脚本. RMAN热备份、非一致性备份脚本. rman名称不允许重复,%U肯定不重复. %D 位于该月中的第几天 (DD). %M 位于该年中的第几月 (MM). %F 一个基于DBID 唯一的名称,这个格式的形式为c-IIIIIIIIII-YYYYMMDD-QQ,. %d 数据库名称其中IIIIIIIIII 为该数据库的DBID,YYYYMMDD 为日期,QQ 是一个1-256 的序列.

linux异步IO浅析

- Sepher - kouu&#39;s home
知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上. 预先知道这些数据的位置,所以预先发起异步IO读请求. 等到真正需要用到这些数据的时候,再等待异步IO完成. 使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情).

Android handler异步更新

- - 博客园_首页
private static final int MSG_SUCCESS = 0;// 获取图片成功的标识. private static final int MSG_FAILURE = 1;// 获取图片失败的标识. mImageView.setImageBitmap((Bitmap) msg.obj);// imageview显示从网络获取到的logo.

Android异步接口测试

- - 百度质量部 | 软件测试 | 测试技术 | 百度测试
    基于Android的C/S移动应用中访问后端数据的场景是非常多的,异步接口测试主要是在单元测试完成的基础上检查接口级访问是否正确,主要保证对外请求的组装与发送是否符合后端的约定. 现在项目的异步接口访问都遵循一个特定的访问模式:前台的Activity获取到触发事件后将接受到的参数传给一个异步任务,这些任务大都是AsyncTask的实现——即启动一个新的线程访问后台接口数据,完毕后调用回调函数更新UI展示,示意图如下:.