做了这么多年前端,你真的了解html中资源加载执行时序吗

蛰伏已久 蛰伏已久 2019-03-09

做了好几年前端了,最近想进个大厂,参加了阿里、百度、今日头条的面试,被彻底的打击了,感觉自己的基础不是很牢,所以萌发了重学前端的想法,向习以为常的知识/技能,多问一个为什么,多了解一下原理,或者源码

今天我们来聊聊html中资源加载和执行的顺序问题。

做了几年前端了,可是当浏览器获取到html文件后怎么加载执行文件,这些问题你能回答吗

  • 按照什么顺序加载文件?

  • 每次能加载多少个文件?

  • css没加载完会执行js吗?

  • async和defer的区别是什么?

  • DOMContentLoaded事件和onload事件,什么时间触发?

为了搞清楚这些问题,我们来做下试验,我用的是chrome浏览器。

html中资源加载顺序及数量

我们先构造一个html文件,在head里面交替加载10个css文件和10个js文件,这几个文件地址是我用本地php服务创建的,都延迟3秒返回数据,以便我们清楚观察效果

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="http://test.com/test/css1">
    <script src="http://test.com/test/js1"></script>
    <link rel="stylesheet" href="http://test.com/test/css2">
    <script src="http://test.com/test/js2"></script>
    <link rel="stylesheet" href="http://test.com/test/css3">
    <script src="http://test.com/test/js3"></script>
    <link rel="stylesheet" href="http://test.com/test/css4">
    <script src="http://test.com/test/js4"></script>
    <link rel="stylesheet" href="http://test.com/test/css5">
    <script src="http://test.com/test/js5"></script>
    <link rel="stylesheet" href="http://test.com/test/css6">
    <script src="http://test.com/test/js6"></script>
    <link rel="stylesheet" href="http://test.com/test/css7">
    <script src="http://test.com/test/js7"></script>
    <link rel="stylesheet" href="http://test.com/test/css8">
    <script src="http://test.com/test/js8"></script>
    <link rel="stylesheet" href="http://test.com/test/css9">
    <script src="http://test.com/test/js9"></script>
    <link rel="stylesheet" href="http://test.com/test/css10">
    <script src="http://test.com/test/js10"></script>
</head>
<body>
</body>
</html>

css和js的地址我是使用php框架构造的,目的是为了使用php的sleep延迟返回资源,好让我们更清楚的看到时序,你可以通过node或其他后台自行构造

   //js文件,等待3s后返回
    public function actionJs1(){
        sleep(3);        return "console.log('js1111111')";
    }    //css文件,等待3s返回
     public function actionCss1(){
        sleep(3);
    }

我们通过chrome来看下加载时序,可以看到,浏览器首先下载html文件,下载之后紧接着按照文档顺序请求css1、js1、css2、js2、css3、js3,可以看到浏览器一次可以加载6个文件,加载完成之后再加载6个,直到全部加载完成。

而加载顺序实际反复测试几次,均为【css1、js1、css2、js2、css3、js3】、【css4、css5、css6、css7、css8、css9】、【css10、js4、js5、js6、js7、js8】、【js9、js10】,可以看出,有两个特点:整体来说是按照文档在html中出现顺序进行加载的,但是会部分优先加载css文件

加载时序

为了证实上面的猜想,有对html代码进行了改造,10个css全放前面,10个js全放后面

   <link rel="stylesheet" href="http://test.com/test/css1">
    ......    
   <link rel="stylesheet" href="http://test.com/test/css10">
    <script src="http://test.com/test/js1"></script>
    ......    
   <script src="http://test.com/test/js10"></script>

果然,浏览器优先把前面10个css先加载完毕,然后才加载js

而如果我们把10个js放到10个css文件前面呢

    <script src="http://test.com/test/js1"></script>
    ......    
    <script src="http://test.com/test/js10"></script>
     <link rel="stylesheet" href="http://test.com/test/css1">
    ......    
    <link rel="stylesheet" href="http://test.com/test/css10">

可以看到,浏览器先加载了6个js,然后又优先把10个css加载完,之后才加载js

接下来在body中再添加10个图片,看看css、js、图片,这三种资源的加载顺序,不用测试我们应该也能想到,图片应该在css和js之后,实验验证一下。

构造测试html,每个资源设置6个,顺序如下

    <link rel="stylesheet" href="http://test.com/test/css1">
    ......   
    <link rel="stylesheet" href="http://test.com/test/css6">

    <img src="http://test.com/test/img1"/>
    ......    
    <img src="http://test.com/test/img6"/>
    <script src="http://test.com/test/js1"></script>
    ......    
    <script src="http://test.com/test/js6"></script>

chrome文件加载时序为,先加载6个css、再加载6个js,最后加载6个图片

浏览器每次只能加载6个文件,那还能不能再多加载一些呢,假如加载的资源域名不同,会发生什么?

    //前六个css域名为test.com    
    <link rel="stylesheet" href="http://test.com/test/css1">
    ......    
    <link rel="stylesheet" href="http://test.com/test/css6">
     //后六个css域名为m.test.com    
    <link rel="stylesheet" href="http://m.test.com/test/css1">
    ......    
    <link rel="stylesheet" href="http://m.test.com/test/css6">

可以看到,浏览器一次性加载了12个文件

结论

1.浏览器加载文件,整体顺序是按照文件在html中出现的顺序进行加载

2.但是会优先加载css、然后加载js、最后加载图片

3.同一个域名的资源,谷歌浏览器每次加载6个,不同域名的资源可以并行加载

看到这里,我们应该会性能优化有了一些想法了吧,比如使用不同域名可以提高整体加载速度

html中js执行时机

js会在加载成功立即执行吗,还是会受到别的因素影响呢?我们先来测试一下吧,假如css延迟3s返回,js取消延迟立即返回,看看会发生什么

 //css延迟3s返回数据 
 <link rel="stylesheet" href="http://test.com/test/css1">
 //js立即返回,console.log('js1') 
 <script src="http://test.com/test/js1"></script>

刷新页面可以看到尽管js很快就加载完成了,但是并没有立即执行,等了3s左右才执行,也就是在css加载完成后才执行,即css的加载会阻塞js的执行,那是不是因为css在js前面的原因呢?

我们调换一下css和js的顺序,则可以看到js立即打印,并不会等待css的加载

//js立即返回,console.log('js1') 
<script src="http://test.com/test/js1"></script>
 //css延迟3s返回数据 
 <link rel="stylesheet" href="http://test.com/test/css1">

结论

  1. 如果css在js前面,则css的加载会阻塞js的执行,即css加载完毕后,执行js

  2. 如果css在js后面,则不会阻塞js的执行

接下来我们看看两个js的加载执行顺序,先从最普通的开始

//js1延迟5s返回数据,console.log('js1') 
<script src="http://test.com/test/js1"></script>
 //js2延迟3s返回数据,console.log('js2')
 <script src="http://test.com/test/js2"></script>
 <script>
    console.log('js3')
 </script>

等待5s后,控制台先后打印 js1  js2 js3,虽然js2提前加载完成,但是仍然要等待前面的js1加载执行完毕才能执行

js异步加载执行有两种方式async和defer,接下来我们给js添加async属性看看

//js1延迟5s返回数据,console.log('js1') 
<script src="http://test.com/test/js1" async></script>
 //js2延迟3s返回数据,console.log('js2')
 <script src="http://test.com/test/js2" async></script>
 <script>
    console.log('js3')
 </script>

控制台立即打印顺序为

js3
js2
js1

可见添加async属性js不会阻塞其后面js的执行,谁先加载完成谁先执行,接下来添加defer属性。

//js1延迟5s返回数据,console.log('js1') 
<script src="http://test.com/test/js1" defer></script>
 //js2延迟3s返回数据,console.log('js2')
 <script src="http://test.com/test/js2" defer></script>
 <script>
    console.log('js3')
 </script>

控制台打印顺序如下,可以看出,添加defer属性的js,不会阻塞后面的js执行,但是多个添加defer的js,仍然按照既有顺序执行。

js3
js1
js2

下面是一张经典js加载执行时机对比图,

  • 浏览器遇到没有添加异步属性的js,会立即加载并执行,也就是说会阻塞html的解析

  • 浏览器遇到添加async属性的js会立即加载(当然了,如谷歌浏览器,就算没有添加async,它也会提前识别html中的js文件进行加载,加载时机如第一部分讨论),并在js加载完毕之后立即执行;多个async属性的js谁先加载完成谁先执行

  • 浏览器遇到添加defer属性的js会立即加载,但是不会立即执行,而是会在html解析完成之后,DOMContentLoaded触发之前执行;多个defer属性的js会按照文档顺序执行

上面提到添加defer属性的js会在文档解析之后,DOMContentLoaded触发之前执行,也就是说,在添加defer属性的js中我们是可以获取到dom的

//js1延迟5s返回数据,console.log(document.getElementById('test')) 
<script src="http://test.com/test/js1" defer></script>
 <script>
    console.log(document.getElementById('test'))
 </script>
 <div id="test"></div>

内联js,在id=“test”的dom之前,所以输出null,而defer属性的js打印出了dom

null
<div id="test"></div>

即在defer属性的js中,可以安全的操作dom

结论

  1. 如果css在js前面,则css的加载会阻塞js的执行,即css加载完毕后,执行js

  2. 如果css在js后面,则不会阻塞js的执行

  3. 非异步js按照顺序进行执行

  4. 多个async属性的js,谁先加载完成谁执行

  5. defer属性的js在文档解析之后,DOMContentLoaded触发之前执行,多个defer属性的js按文档顺序执行

DOMContentLoaded和onload

网上查看一些资料,一般说法是这样的

1、当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。

2、当 DOMContentLoaded事件触发时,仅当DOM加载完成,不包括样式表,图片,flash。

但是通过测试发现并非如此,假设css延迟3s返回,图片延迟5s返回,我们看看打印

 //css延迟3s返回数据 
 <link rel="stylesheet" href="http://test.com/test/css1">
 <script>
    document.addEventListener('DOMContentLoaded',function () {        
    console.log('DOMContentLoaded')
    },false)    
    
    window.onload=function () {        
    console.log("onload")
    }
 </script>
 //延迟5s返回数据
 <img src="http://3w.com/test/img1"/>

可以看出3s之后才打印DOMContentLoaded,5s之后打印onload,也就是说DOMContentLoaded是需要等待css加载完成的
我们刚才前面说了,defer属性的js要在DOMContentLoaded之前执行,那么假如defer属性延迟3s返回呢,我们看看效果

<script>
    document.addEventListener('DOMContentLoaded',function () {        
        console.log('DOMContentLoaded')
    },false)    
    window.onload=function () {        
        console.log("onload")
    }
</script>
//js1延迟3s返回数据 
<script src="http://test.com/test/js1" defer></script>
//延迟5s返回数据
<img src="http://3w.com/test/img1"/>

同样的也在3s之后打印DOMContentLoaded,5s之后打印onload,可以说网上的结论和实际是不相符的

结论

  1. 当 页面上所有的DOM,样式表,脚本,图片,flash都加载完成之后触发onload。

  2. 当DOM加载解析完成、css加载完成、内联js执行完成、defer属性的js完成才会触发DOMContentLoaded,图片和async属性的js,不会阻止发DOMContentLoaded的加载。

同样的这些结论也给我们做前端优化提供一些思路,比如给js添加async属性、减少js及css大小、使用懒加载减少图片请求,使其尽快进入onload事件等等

有兴趣的同学欢迎关注公众号,让我们一起重学前端,夯实基础。

分享到

点赞(2)