`
TracyLin
  • 浏览: 4620 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

浏览器环境下JavaScript脚本加载与执行探析之defer与async特性

阅读更多

defer和async特性相信是很多JavaScript开发者"熟悉而又不熟悉"的两个特性,从字面上来看,二者的功能很好理解,分别是"延迟脚本"和"异步脚本"的作用。然而,以defer为例,一些细节问题可能开发者却并不一定熟悉,比如:有了defer特性的脚本会延迟到什么时候执行;内部脚本和外部脚本是不是都能够支持defer;defer后的脚本除了会延迟执行之外,还有哪些特殊的地方等等。本文结合已有的一些文章以及MDN文档中对两个特性的阐述,对defer和async进行更全面的研究和总结,希望能够帮助开发者更好地掌握这两个特性。

1 引言

在《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》中我们提到过,JavaScript代码的执行会阻塞页面的解析渲染以及其他资源的下载,当然由于JavaScript是单线程语言,那就意味着在正常情况下,一个页面中的JavaScript代码只能按顺序从上到下执行,当然,正如《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》中我们分析的,在某些情况下,比如通过document.write进入脚本或者通过动态脚本技术引入脚本时,JavaScript代码的执行顺序不一定严格按照从上到下的顺序,而defer和async也是我们所说的"非正常的情况"。

我们经常会说JavaScript的执行具有阻塞性,而在实际的开发中,我们通常最关心的阻塞,同时也是最影响用户体验的阻塞应该是以下几个方面:

[1]页面解析和渲染的阻塞

[2]我们写的页面初始化脚本(一般是监听DOMContentLoaded事件所绑定的脚本,这部分脚本是我们希望最先执行的脚本,因为我们会把和用户交互最相关的代码写在这里)

[3]页面外部资源下载的阻塞(比如图片)

如果我们有一个耗时的脚本操作,而这段脚本又阻塞了上面我们提到的这三个地方,那么这个网页的性能或者用户体验就非常差了。

defer和async这两个特性的初衷也是希望能够解决或者缓解阻塞对于页面体验的影响,下面我们就来分析一下这两个特性,我们主要从以下几个方面来全方位了解这两个特性:

[1]延迟或异步的脚本的执行时机是什么时候?对于页面的阻塞情况如何?

[2]内部脚本和外部脚本是否都能够实现延迟或异步?

[3]浏览器对这两个特性的支持情况如何?有没有相关的bug?

[4]使用了这两个特性的脚本在使用时还有什么需要注意的地方?

2 defer特性

2.1 关于defer脚本的执行时机

defer特性是HTML4规范中定义的扩展特性,最初只有IE4+和firefox3.5+才支持,之后chrome等浏览器也增加了对它的支持,使用的方式为defer="defer"。defer意为延迟,也就是会延迟脚本的执行。正常情况下,我们引入的脚本会被立即下载和执行,而有了defer特性之后,脚本下载完毕后不会立即执行,而是等到页面解析完毕之后再执行。我们看一下HTML4标准对defer的阐述:

defer:When set, this boolean attribute provides a hint to the user agent that the script is not going to generate any document content (e.g., no "document.write" in javascript) and thus, the user agent can continue parsing and rendering.

也就是说,如果设置了defer,那么就告诉用户代理,这个脚本不会产生任何文档内容,从而用户代理可以继续解析和渲染。我们再看一下MDN中对defer的关键描述:

defer:If the async attribute is not present but the defer attribute is present, then the script is executed when the page has finished parsing. 

通过标准中的定义,我们可以明确,即:defer的脚本不会阻塞页面的解析,而是等到页面解析结束之后再执行,但是耗时的defer依然可能会阻塞外部资源的下载,那么它会阻塞DOMContentLoaded事件么?事实上,defer的脚本依然是在DOMContentLoaded事件之前执行的,因此它还是会阻塞DOMContentLoaded中的脚本。我们可以通过下图来帮助理解defer脚本的执行时机:

来源:https://html.spec.whatwg.org/multipage/scripting.html#attr-script-defer

根据标准中的定义,内部脚本不支持defer,而IE9及以下的浏览器则提供了内部脚本的defer支持

2.2 defer的浏览器支持情况

下面我们来看一下defer特性的浏览器支持情况:

IE9及以下的浏览器存在一个bug,这个bug将在稍后的DEMO中进行详细的说明。

2.3 DEMO:defer特性的功能验证

我们模仿在Olivier Rochard在《the script defer attribute》使用的方式来验证一下defer特性的功能:

首先我们准备了6个外部脚本:

1.js:

test += "我是head外部脚本\n";

 2.js

test += "我是body外部脚本\n";

 3.js

test += "我是底部外部脚本\n";

 defer1.js

test += "我是head外部延迟脚本\n";

 defer2.js

test += "我是body外部延迟脚本\n";

defer3.js

test += "我是底部外部延迟脚本\n";

 HTML中的代码为:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>defer attribute test</title>
    <script src="http://lib.sinaapp.com/js/jquery/1.9.1/jquery-1.9.1.min.js"></script>
    <script type="text/javascript">var test = "";</script>
    <script src="defer1.js" type="text/javascript" defer="defer"></script>
    <script src="1.js" type="text/javascript"></script>
    <script defer="defer">
        test += "我是head延迟内部脚本\n";
    </script>
    <script>
        test += "我是head内部脚本\n";
    </script>
</head>
<body>
<button id="test">点击一下</button>
<script src="defer2.js" type="text/javascript" defer="defer"></script>
<script src="2.js" type="text/javascript"></script>
</body>
<script src="defer3.js" type="text/javascript" defer="defer"></script>
<script src="3.js" type="text/javascript"></script>
<script>
    $(function(){
        test += "我是DOMContentLoaded里面的脚本\n";
    })
    window.onload = function(){
        test += "我是window.onload里面的脚本\n";
        var button = document.getElementById("test");
        button.onclick = function(){
            alert(test);
        }
    }
</script>
</html>

 代码中,为了方便实现DOMContentLoaded事件,我们引入了jQuery(之后的文章还会再介绍如何自己实现兼容的DOMContentLoaded),然后,我们在脚本的head内、body内部和body外部分别引入延迟脚本和正常脚本,并且通过一个全局的字符串来记录每一段代码的执行状态,我们看一下各个浏览器中的执行结果:

IE7 IE9 IE10 CHROME firefox

我是head外部脚本
我是head内部脚本
我是body外部脚本
我是底部外部脚本
我是head外部延迟脚本
我是head延迟内部脚本
我是body外部延迟脚本
我是底部外部延迟脚本
我是DOMContentLoaded里面的脚本
我是window.onload里面的脚本

 

我是head外部脚本
我是head内部脚本
我是body外部脚本
我是底部外部脚本
我是head外部延迟脚本
我是head延迟内部脚本
我是body外部延迟脚本
我是底部外部延迟脚本
我是DOMContentLoaded里面的脚本
我是window.onload里面的脚本

 

我是head外部脚本
我是head延迟内部脚本
我是head内部脚本
我是body外部脚本
我是底部外部脚本
我是head外部延迟脚本
我是body外部延迟脚本
我是底部外部延迟脚本
我是DOMContentLoaded里面的脚本
我是window.onload里面的脚本

我是head外部脚本
我是head延迟内部脚本
我是head内部脚本
我是body外部脚本
我是底部外部脚本
我是head外部延迟脚本
我是body外部延迟脚本
我是底部外部延迟脚本
我是DOMContentLoaded里面的脚本
我是window.onload里面的脚本

我是head外部脚本
我是head延迟内部脚本
我是head内部脚本
我是body外部脚本
我是底部外部脚本
我是head外部延迟脚本
我是body外部延迟脚本
我是底部外部延迟脚本
我是DOMContentLoaded里面的脚本
我是window.onload里面的脚本

 从输出的结果中我们可以确定,只有IE9及以下浏览器支持内部延迟脚本,并且defer后的脚本都会在DOMContentLoaded事件之前触发,因此也是会堵塞DOMContentLoaded事件的。

2.4 DEMO:IE<=9的defer特性bug

从2.3节中的demo可以看出,defer后的脚本还是能够保持执行顺序的,也就是按照添加的顺序依次执行。而在IE<=9中,这个问题存在一个bug:假如我们向文档中增加了多个defer的脚本,而且之前的脚本中有appendChild,innerHTML,insertBefore,replaceChild等修改了DOM的接口调用,那么后面的脚本可能会先于该脚本执行。可以参考github的issue:https://github.com/h5bp/lazyweb-requests/issues/42

我们通过DEMO验证一下,首先修改1.js的代码为(这段代码只为模拟,事实上这段代码存在极大的性能问题):

document.body.innerHTML = "<div id='div'>我是后来加入的</div>";
document.body.innerHTML += "<div id='div'>我是后来加入的</div>";
document.body.innerHTML += "<div id='div'>我是后来加入的</div>";
document.body.innerHTML += "<div id='div'>我是后来加入的</div>";
document.body.innerHTML += "<div id='div'>我是后来加入的</div>";
document.body.innerHTML += "<div id='div'>我是后来加入的</div>";
document.body.innerHTML += "<div id='div'>我是后来加入的</div>";
alert("我是第1个脚本");

 2.js

alert("我是第2个脚本");

 修改HMTL中的代码为:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>defer bug in IE=9 test</title>
    <script src="1.js" type="text/javascript" defer="defer"></script>
    <script src="2.js" type="text/javascript" defer="defer"></script>
</head>
<body>
</body>
</html>

 正常情况下,浏览器中弹出框的顺序肯定是:我是第1个脚本-》我是第2个脚本,然而在IE<=9中,执行结果却为:我是第2个脚本-》我是第1个脚本,验证了这个bug。

2.5 defer总结

在总结之前,首先要说一个注意点:正如标准中提到的,defer的脚本中不应该出现document.write的操作,浏览器会直接忽略这些操作。

总的来看,defer的作用一定程度上与将脚本放置在页面底部有一定的相似,但由于IE<=9中的bug,如果页面中出现多个defer时,脚本的执行顺序可能会被打乱从而导致代码依赖可能会出错,因此实际项目中很少会使用defer特性,而将脚本代码放置在页面底部可以替代defer所提供的功能。

3 async特性

3.1 关于async脚本的执行时机

async特性是HTML5中引入的特性,使用方式为:async="async",我们首先看一下标准中对于async特性的相关描述:

async:If the async attribute is present, then the script will be executed asynchronously, as soon as it is available. 

需要指出,这里的异步,指的其实是异步加载而不是异步执行,也就是说,浏览器遇到一个async的script标签时,会异步的去加载(个人认为这个过程主要是下载的过程),一旦加载完毕就会执行代码,而执行的过程肯定还是同步的,也就是阻塞的。我们可以通过下图来综合理解defer和async:

来源:https://html.spec.whatwg.org/multipage/scripting.html#attr-script-defer

这样来看的话,async脚本的执行时机是无法确定的,因为脚本何时加载完毕也是不确定的。我们通过下面的demo来感受一下:

async1.js

alert("我是异步的脚本");

 HTML代码:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>async attribute test</title>
    <script src="/delayfile.php?url=http://localhost/js/load/async1.js&delay=2" async="async" type="text/javascript"></script>
    <script>
        alert("我是同步的脚本");
    </script>
</head>
<body>
</body>
</html>

 这里我们借用了《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》中的delayfile脚本来提供了一个延迟,这个脚本在支持async的浏览器中,弹框的顺序一般是:我是同步的脚本-》我是异步的脚本。

3.2 async的浏览器支持情况

下面我们来看一下async特性的浏览器支持情况:

 

 可以看到,只有IE10+才支持async特性,opera mini不支持async特性,另外,async是不支持内部脚本的。

3.3 async总结

async指的异步脚本,即脚本异步加载,加载的过程不会造成阻塞,但是async的脚本的执行时机是不确定的,而且执行的顺序也是不确定的,因此使用async的脚本应该是不依赖于任何代码的脚本(比如第三方统计代码或广告代码),否则就会导致执行出错。

4 defer和async的优先级问题

这一点比较好理解,标准中规定了:

[1]如果<script>元素同时定义了defer和async特性,则按async来处理(注意:对于不支持async的浏览器会直接忽略async特性)

[2]如果<script>元素只定义了defer,则按延迟脚本的方式处理

[3]如果<script>元素没有定义defer也没有定义async,则按正常情况处理,即:脚本立即加载和执行

 

由于经验尚浅,文中难免会出现错误,欢迎指正错误和交流~~

2
0
分享到:
评论

相关推荐

    浏览器环境下JavaScript脚本加载与执行探析之动态脚本与Ajax脚本注入

    在浏览器环境下,JavaScript脚本加载与执行的方式多样,其中动态脚本和Ajax脚本注入是两种常见的优化策略,旨在减少脚本对页面解析和渲染的影响。动态脚本是指通过JavaScript动态创建`&lt;script&gt;`标签并插入到DOM中,...

    JavaScript提高加载和执行效率的方法

    异步加载与延迟执行异步加载(async)和延迟执行(defer)是两种可以改善JavaScript加载和执行效率的技术。 1. 异步加载(async):通过在标签中添加async属性,可以让浏览器非阻塞地下载JavaScript文件,即在下载...

    浅谈HTML5 defer和async的区别

    HTML5的`&lt;script&gt;`元素提供了两个可选的属性,`defer`和`async`,它们主要用于控制外部JavaScript脚本的加载和执行方式。这两者的设计目标都是为了优化页面加载性能,避免因为脚本的执行阻塞页面的渲染。 1. **没有...

    async与defer的区别

    形象的描述了async与defer的区别,...defer是延迟执行,在浏览器看起来的效果像是将脚本放在了 body后面一样(虽然按规范应该是在 DOMContentLoaded事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)

    js的[defer]和[async]属性

    JS中defer和async属性是HTML文档加载和执行外部JavaScript文件时的重要关键字,它们指示浏览器如何处理脚本与页面内容的加载顺序和执行时机。了解这两个属性对于编写高性能的网页至关重要。 首先,我们需要了解...

    06-defer和async.md

    在前端技术领域,理解`&lt;script&gt;`标签的`defer`和`async`属性的区别是开发中常见的需求,这关系到页面加载性能和脚本执行顺序。本篇文档围绕这两个属性进行解释,并扩展到前端面试相关的知识点。 ### defer和async的...

    引入JavaScript脚本代码到HTML文档中

    但是,这种特性需要服务器环境支持,并且可能不被所有浏览器完全兼容。 总结来说,引入JavaScript到HTML文档是为了实现动态交互,可以通过内联、内部脚本和外部脚本三种方式实现。为了保持代码组织和提高性能,推荐...

    编写高性能的JavaScript 脚本的加载与执行

    这两种方式可以进一步优化脚本加载执行的时间,不过要注意它们只适用于外部脚本。 为了进一步提升性能,开发者还需要考虑以下几点: 1. 减少脚本数量:尽量合并多个脚本到一个文件中,这样可以减少HTTP请求的数量...

    JavaScript 的性能优化:加载和执行.docx

    浏览器中的JavaScript执行具有阻塞特性,意味着在执行脚本时,浏览器无法进行其他操作,如下载资源或渲染页面。这直接影响用户体验,特别是当JavaScript代码执行时间过长时。 首先,理解JavaScript阻塞的原因是优化...

    如何让动态插入的javascript脚本代码跑起来

    `defer`属性是一个可选的特性,它告诉浏览器在文档解析完成后,但在`DOMContentLoaded`事件触发之前执行脚本。这可以帮助确保脚本按顺序执行,但并不是所有浏览器都支持。 在实际应用中,我们需要考虑脚本加载状态...

    JavaScript无阻塞加载和defer、async详解

    在旧的浏览器下,都是按照先后顺序来加载的,这就保证了加载的js依赖不会发生问题。但是少部分新的浏览器已经开始允许并行加载js了,也就是说可以同时下载js文件,但是还是按先后顺序执行文件的。 下载是异步的没...

    浅析script标签中的defer与async属性

    - `async`更适合那些可以独立于页面加载执行且不依赖其他脚本的库,比如Google Analytics或百度统计等。 **浏览器行为差异** 虽然规范定义了`defer`和`async`的行为,但实际浏览器的实现可能会有所不同。例如,...

    javascript函数动态加载javascript文件

    `async`属性使得脚本在可用时立即加载,但不保证执行顺序。`defer`属性则确保脚本在文档解析完成后、DOMContentLoaded事件触发之前执行。 2. **使用`XMLHttpRequest`**: JavaScript的`XMLHttpRequest`对象允许...

    跟我学习javascript的异步脚本加载

    在互联网应用日益增多的今天,网页加载速度对于用户体验的影响愈加重要。在提升网页加载速度的诸多方法中,合理地异步加载JavaScript脚本...开发者们需要不断优化脚本加载方式,以适应不断变化的浏览器特性和用户需求。

    5种JavaScript脚本加载的方式

    在前端开发中,JavaScript脚本的加载方式对页面性能有着重要的影响。为了提高页面的加载速度和执行效率,开发者通常会采取不同的策略来控制JavaScript文件的加载顺序和时机。本文介绍了五种常见的JavaScript脚本加载...

    javascript延时加载之defer测试

    JavaScript中的`defer`属性是一种延迟加载机制,它允许开发者控制脚本执行的时机,以便优化网页性能。在HTML文档解析到`&lt;script&gt;`标签时,通常会暂停文档的解析,执行脚本,然后继续解析。这可能导致页面加载变慢,...

    关于Javascript中defer和async的区别总结

    没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。 [removed][removed] 有 async,加载和...

    HTML5 script元素async、defer异步加载使用介绍

    与`async`不同,`defer`属性保证了脚本会按照在HTML文档中的顺序执行,但脚本的执行会等到整个HTML文档解析完毕,DOM构建完成,即在`DOMContentLoaded`事件触发之前。这样既保持了执行顺序,又避免了脚本阻塞HTML...

Global site tag (gtag.js) - Google Analytics