浏览器是如何解析html的?

12,623 阅读8分钟

当我们在浏览器地址栏输入一个合法的url时,浏览器首先进行DNS域名解析,拿到服务器IP地址后,浏览器给服务器发送GET请求,等到服务器正常返回后浏览器开始下载并解析html。这里仅总结浏览器解析html的过程。

html页面主要由domcssjavascript等部分构成,其中cssjavascript既能内联也能以脚本的形式引入,当然html中还可能引入imgiframe等其他资源。其实所有的这些资源也是以dom标签的形式嵌入在html页面中的,因此本篇总结说的html解析过程就是dom的解析过程。

1 dom解析过程

整个dom的解析过程是顺序,并且渐进式的。

顺序指的是从第一行开始,一行一行依次解析;渐进式则指得是浏览器会迫不及待的将解析完成的部分显示出来,如果我们做下面这个实验会发现,在断点处第一个div已经在浏览器渲染出来了:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
    <div>
        first div
    </div>
    <script>
        debugger
    </script>
    <div>
        second div
    </div>
</body>
</html>

既然dom是从第一行按顺序解析,那么我们怎么判断dom何时解析完成呢?这个问题应该经常会在面试中问到,比如一般会问:

window.onloadDOMContentLoaded有什么区别?

其实就是想看看是不是明白dom树何时构建完成,这个问题确实很重要,尤其是对于几年前的jquery技术栈来说,因为我们使用javascript操作dom或者给dom绑定事件有个前提条件就是需要dom树已经创建完成。整个html页面的dom解析完成时,dom树也就构建完成了。dom树构建完成后document对象会派发事件DOMContentLoaded来通知dom树已构建完成。

html从第一行开始解析,遇到外联资源(外联css外联javascriptimageiframe等)就会请求对应资源,那么请求过程是否会阻塞dom的解析过程呢?答案是看情况,有的资源会,有的资源不会。下面按是否会阻塞页面解析分为两类:阻塞型非阻塞型,注意这里区分两类资源的标志是document对象派发DOMContentLoaded事件的时间点,认为派发DOMContentLoaded事件才表示dom树构建完成。

1.1 阻塞型

会阻塞dom解析的资源主要包括:

  • 内联css
  • 内联javascript
  • 外联普通javascript
  • 外联defer javascript
  • javascript标签之前的外联css

外联javascript可以用asyncdefer标示,因此这里分为了三类:外联普通javascript外联defer javascript外联async javascript,这几类外联javascript本篇后面有详细介绍。 dom解析过程中遇到外联普通javascript会暂停解析,请求拿到javascript并执行,然后继续解析dom树

对于外联defer javascript这里重点说明下为什么也归于阻塞型。前面也说了,这里以document对象派发DOMContentLoaded事件来标识dom树构建完成,而defer javascript是在该事件派发之前请求并执行的,因此也归类于阻塞型,但是需要知道,deferjavascript实际上是在dom树构建完成与派发DOMContentLoaded事件之间请求并执行的,不过如果换个思路理解,<script>本身也是dom的一部分也就不难理解为什么deferjavascript会在DOMContentLoaded派发之前执行了。

另外需要注意的是javascript标签之前的外联css。其实按说css资源是不应该阻塞dom树的构建过程的,毕竟css只影响dom样式,不影响dom结构,MDN上也是这么解释的:

The DOMContentLoaded event is fired when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.

但是实际情况是dom树的构建受javascript的阻塞,而javascript执行时又可能会使用类似Window.getComputedStyle()之类的API来获取dom样式,比如:

const para = document.querySelector('p');
const compStyles = window.getComputedStyle(para);

因此浏览器一般会在遇到<script>标签时将该标签之前的外联css请求并执行完成。但是注意这里加了一个前提条件就是javascript标签之前的外联css,就是表示被javascript执行依赖的外联css。这个容易忽略的点这篇文章也有说明,推荐阅读。

这些阻塞型的资源请求并执行完之后dom树的解析便完成了,这时document对象就会派发DOMContentLoaded事件,表示dom树构建完成。

1.2 非阻塞型

不阻塞dom解析的资源主要包括:

  • javascript标签之后的外联css
  • image
  • iframe
  • 外联async javascript

dom树解析完成之后会派发DOMContentLoaded事件,对于外联css资源来说分为两类,一类是位于<script>标签之前,一类是位于<script>标签之后。位于<script>标签之后的外联css是不阻塞dom树的解析的。外联cssdom树解析过程的影响这里有一篇非常好的文章介绍:DOMContentLoaded and stylesheets,推荐阅读。

DOMContentLoaded事件用来标识dom树构建完成,那如何判断另外这些非阻塞型的资源加载完成呢?答案是window.onload。由于该事件派发的过晚,因此一般情况下我们用不着,而更多的是用DOMContentLoaded来尽早的的操作dom

另外还有imageiframe以及外联async javascript也不会阻塞dom树的构建。这里外联async javascript又是什么呢?下一节整体介绍下外联javascript

2 外联javascript加载过程

html页面中可以引入内联javascript,也可以引入外联javascript外联javascript又分为:

  • 外联普通javascript
<script src="indx.js"></script>
  • 外联defer javascript
<script defer src="indx.js"></script>
  • 外联async javascript
<script async src="indx.js"></script>

其中第一种就是外联普通javascript,会阻塞html的解析,html解析过程中每遇到这种<script>标签就会请求并执行,如下图所示,绿色表示html解析;灰色表示html解析暂停;蓝色表示外联javascript加载;粉色表示javascript执行

标记
外联普通javascript的加载执行过程如下:
外联普通javascript
第二种外联defer javascript稍有不同,html解析过程中遇到此类<script>标签不阻塞解析,而是会暂存到一个队列中,等整个html解析完成后再按队列的顺序请求并执行javascript,但是这种外联defer javascript全部加载并执行完成后才会派发DOMContentLoaded事件,外联defer javascript的加载执行过程如下:
外联defer javascript
第三种外联async javascript则不阻塞html的解析过程,注意这里是说的脚本的下载过程不阻塞html解析,如果下载完成后html还没解析完成,则会暂停html解析,先执行完成下载后的javascript代码再继续解析html,过程如下:
外联async javascript
但是如果html已经解析完毕,外联async javascript还未下载完成,则不阻塞DOMContentLoaded事件的派发。因此外联async javascript很有可能来不及监听DOMContentLoaded事件,比如stackoverflow上的这个问题

说明下,这几个图引用自这里

3 DOMContentLoaded兼容性问题

DOMContentLoaded最开始由firefox提出,其他浏览器觉得非常有用也相继开始支持,但是特性却稍有不同,比如operajavascript的执行并不等待外联css的加载。直到HTML5出来后将DOMContentLoaded标准化,依照HTML5标准,javascript脚本执行前,出现在当前<script>之前的<link rel="stylesheet">必须完全载入。

那么在所有浏览器标准化之前怎么解决DOMContentLoaded的兼容性问题呢?可以参考jQuery.ready()方法的实现,对于该方法的源码分析网上已经一大堆了,这里就不做分析了,直接说下原理。其实是就是用了MDN: DOMContentLoaded中介绍的兼容性方法,ie9才开始支持DOMContentedLoadedie8环境可以通过检测document.readystate状态来确认dom树是否构建完成。document.readystate包括3种状态:

  • loading - html文档加载中
  • interactive - html文档加载并解析完成,但是图片等资源还未完成加载,相当于DOMContentLoaded
  • complete - 所有资源加载完成,相当于window onload

因此我们通过判断document.readystate的状态为interactive来模拟DOMContentLoaded时间点。但是这里需要注意一点,以.ready()方法为例,我们可能在下面这几个地方调用:

  • 内联javasctipt
  • 外联普通javascript
  • 外联defer javascript
  • 外联async javascript

其中3三个地方直接判断document.readystate肯定是loading状态,只有外联async javascript可能出现document.readystateinteractivecompleted的状态,因为外联async javascript是不阻塞dom解析的,因此为了完全覆盖前面的4种情况,需要监听document.readystate的变化:

if (document.readystate === 'interactive'
    || document.readystate === 'complete') {
        // 调用ready回调函数
} else {
    document.onreadystatechange = function () {
        if (document.readystate === 'interative') {
            // 调用ready回调函数
        }
    } 
}

4 引用

主要参考了以下文章,推荐阅读:

  1. Page lifecycle: DOMContentLoaded, load, beforeunload, unload
  2. DOMContentLoaded and stylesheets
  3. script标签: async vs defer attributes
  4. MDN: DOMContentLoaded
  5. MDN: readystatechange
  6. Replace jQuery’s Ready() with Plain JavaScript