script标签添加defer和async的作用,是否可避免页面的解析渲染的阻塞?

950 阅读7分钟

每次被问到defer/async的使用,都需要google一番,所以这次重新再看了一遍,为避免自己再次忘记,乖乖记录一下。

本文仅作个人学习记录用,如有错误之处,请大佬指出!

我们知道script标签放在<head>标签内会造成页面渲染阻塞,所以一般我们都会在script引入外部资源的标签放在body标签末尾</body>前,或者为script标签添加defer或async属性,异步加载外部资源,避免阻塞。defer和async具体是怎么产生作用的呢,本文带你一步一步进行分析。

1.普通script标签,放</body>

<body>
    <div class="box1">a</div>

    <script src="./index.js"></script>
</body>
function delay(time) {
  const now = Date.now();
  while (Date.now() - now < time) {}
}

function init() {
  console.log(11);
  delay(2000);
  console.log(2);
}
init();

浏览器会启动一个预解析线程,预解析线程会大致浏览html文件,提前(开始解析html之前)下载外部的js、css文件,实际交给网络进程帮忙下载。 获取html文件后,浏览器渲染主线程开始从上到下解析html(Parse HTML)。当index.js文件下载完成,渲染主线程会停止解析html,开始执行index.js文件中的js代码。index.js文件中的js代码执行完后,继续解析html。 1.png

外部js的下载、执行会阻塞DOM的解析、渲染?

由于放在</body>前,当渲染主线程遇到script标签时,已经解析了script标签前面的所有DOM元素,且浏览器为了更好的用户体验,不需要所有DOM解析完才开始渲染,所以解析完的这部分DOM元素就会开始渲染,绘制在页面上(然后,渲染主线程会等待js下载完成、并执行完成,如果后面还有dom元素,会继续解析后面的html、渲染,等所有html解析完后,此时才会触发DOMContentLoaded事件,代表所有DOM解析完毕)。

但这部分DOM元素渲染呈现到页面的时机又有两种情况,取决于遇到script标签时,该script标签引入的外部js文件是否已经下载完成: 第一种情况,如果解析到script标签,发现其引入的外部js文件已经下载完成,下载完成的js文件就会将js代码交给渲染主线程,渲染主线程就会停止解析、渲染操作,开始执行js代码,此时js代码就会阻塞解析渲染。

2.png

3.png

第二种情况,若js文件下载速度慢,渲染主线程解析了部分DOM,然后遇到script标签,发现其引入的外部js文件还未下载完成,就会先将已经解析好的DOM进行渲染。我们将浏览器的网络速度改为Fast 3G,模拟请求响应时间长的情况。下面的图可以看到解析完DOM后直接开始渲染了,这种情况可以说外部js的下载、执行就不会造成阻塞。 4.png 但是,如果script标签后面还有其他DOM元素,那么这部分DOM的解析就会被阻塞,等到外部js文件加载并执行完才会开始解析、渲染

<body>
    <div class="box1">a</div>

    <script src="./index.js"></script>

    <h1>b</h1>
</body>

5.png

2、普通script标签,放</head>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>

  <script src="./index.js"></script>
</head>
<body>
  <div class="box1">a</div>
</body>

获取到html文件后,开始解析html,遇到script标签,停止解析html,等待script标签引用的外部js文件下载完成,并执行完成后,才会继续解析后面的html(即body标签内的DOM元素),解析完,开始样式计算、布局、分层、绘制 2.1.png 2.2.png 所以js的下载、执行,会阻塞DOM的解析、渲染。

这里的下载也是由预解析线程提前下载。渲染主线程停止解析html等待js下载,是当解析到script标签了,发现js文件还在下载中,所以这里才会有一个等待其下载的阶段,而不是遇到script标签才开始下载其引入的外部文件。

2.3.png

3、script标签,添加defer属性,放在</head>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>

  <script defer src="./index.js"></script>
</head>

defer-推迟,即渲染主线程从上到下开始解析,遇到script标签,不会等引入的外部js下载并执行完,而是直接继续后续html解析。这种情况等同于上面的第1点的情况,相当于放在</body>前。解析完整个文档,发现script标签引入的外部js文件已经加载完,会立即执行js,执行完后再做后续的渲染;若还在加载中,则渲染页面。 加载完的外部js文件,会等整个文档解析完,且在DOMContentLoaded事件触发前执行。另外,添加了defer属性的script标签引入的js文件会按加载顺序执行。

3.1.png

3.2.png

4、script标签,添加async属性,放在</head>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>

  <script async src="./index.js"></script>
</head>

async-异步,即渲染主线程遇到script标签,不会等引入的外部js下载并执行完,而是继续后续的html解析,和defer一样。和defer不同的是,添加async属性的script引用的外部js文件只要下载完成就会立即执行,即在解析文档过程中(不需要整个文档全部解析完),发现有外部js加载完成,就会立即去执行外部js代码,停止解析和渲染。所以有可能渲染会被js的执行阻塞,js执行完后,继续回来渲染解析了的DOM元素。 整个文档解析完,就会触发DomeContentLoaded事件,不像defer需要等添加defer属性的script的外部js执行完才触发。

4.1.png

添加了async属性的外部js加载后执行的顺序不固定,且外部js的执行不需要等整个文档解析完,而是外部js加载完就执行。 4.2.png 渲染情况如下: 4.3.png 上图由于</body>中的script引入的外部index2.js阻塞了html解析(index2.js需要下载并执行完,才会继续后续的html解析),所以整个文档解析完成时间这么长,也导致DCL很久才触发。而index2.js(init2函数)的执行在index.js(init函数)之后执行,是由于index.js下载完成立即交给了渲染主线程执行。

总结

我们通常说的为script标签添加async,defer属性来异步加载js文件,避免外部js的加载和执行阻塞页面的解析、渲染。实际上,外部js文件的加载,是浏览器启动的一个预解析线程执行的,所以外部js文件的加载即使没有添加async,defer属性也是异步的,且它们是网络进程下载的,不会对渲染进程中的渲染主线程的解析渲染造成影响。添加async,defer属性,可以让渲染主线程在遇到script标签时不需要等待其外部js文件的下载和执行,而是可以继续后续的解析。至于是否会造成阻塞,主要看渲染主线程解析完DOM元素后,开始渲染前,外部js文件是否已下载完成,若下载完成,网络进程会将js代码交给渲染主线程,渲染主线程就会停止解析渲染,转而去执行js代码;如果还未下载完成,渲染主线程解析完DOM后就开始渲染。

所以,async,defer主要解决的是渲染主线程从上到下解析的过程中,遇到script标签,不暂停后面DOM元素的解析,在一定程度上不受js文件下载执行阻塞。但如果渲染主线程还没开始渲染前(解析中或即将准备渲染),外部js文件下载完成,就会去执行外部js代码,此时js的执行就会阻塞页面渲染。可以说,js的下载是一定不会阻塞解析和渲染的,下载完成后的执行有可能会造成阻塞。一般情况下,我们都是线上环境,外部js资源下载会有一定的时间,所以一般解析完html后就会渲染页面,不需要等待外部js的执行,避免被阻塞。但不可避免解析过程中或渲染前,下载完外部js资源,所以为了保证页面渲染效率,我们的js代码尽量避免耗时的操作。

参考

js 和 css 的阻塞问题分析