什么时候解析HTML DOM树?

时间:2015-12-14 14:28:15

标签: javascript html dom

我总是看到网页的渲染流程,如下图所示: enter image description here

所以这幅画只在解析DOM树并创建CSSOM后才开始,对吧?另一种说法是,将<script>放在<body>的末尾是最佳做法,以便在下载脚本之前页面呈现内容。

我的问题是,解析DOM树的时间何时发生?我们怎么说它已经完成了?根据我的理解,<script>最终也是 DOM树的一部分,只有加载了脚本才能调用DOM树。浏览器从上到下读取html文件,创建DOM树,当它看到<script>时,它停止下载并执行它,直到解析遍历整个页面。或者,页面是否在解析DOM树的同时绘制页面?

3 个答案:

答案 0 :(得分:11)

TL; DR:收到文件后,解析开始瞬间

解析和绘画

有关更详细的解释,我们需要深入了解渲染引擎的工作方式。

渲染引擎解析HTML文档并创建两个树:content treerender tree。内容树包含所有DOM节点。渲染树包含所有样式信息(CSSOM)和渲染页面所需的DOM节点。

一旦创建了渲染树,浏览器就会经历两个过程:每个DOM节点应用layoutpainting。应用布局意味着计算DOM节点应出现在屏幕上的精确坐标。绘画意味着实际渲染像素并应用风格属性。

这是一个渐进的过程:浏览器不会等到所有HTML都被解析。将解析和显示部分内容,同时继续处理来自网络的其余内容。

您可以在浏览器中看到此过程。例如,打开Chrome开发者工具并加载您选择的网站。

Network tab

Network标签中记录活动后,您会注意到在下载文档时解析开始。它识别资源并开始下载它们。蓝色垂直线表示DOMContentLoaded事件,红色垂直线表示load事件。

Timeline tab

记录时间表可让您更深入地了解幕后发生的事情。我已将上面的屏幕截图作为示例包含,以指示在解析文档时发生绘画。请注意,初始绘制就在它继续解析文档的另一部分之前发生。此过程一直持续到文档结束。

单线程

渲染引擎是单线程。除了网络操作之外,几乎所有事情都发生在这个线程中。

将其与网络的同步特性相结合。开发人员希望立即解析并执行<script>(即:解析器到达脚本标记时)。这意味着:

  1. 必须从网络中提取资源(由于DNS查找和连接速度,这可能是一个缓慢的过程。)
  2. 资源的内容将传递给Javascript解释器。
  3. 解释器解析并执行代码。
  4. 解析文档会暂停,直到此过程完成。通过在文档末尾包含<script>,您不会改善总解析时间。它确实增强了用户体验,因为解析和绘制过程不会被需要执行的<script>中断。

    通过使用defer和/或async标记资源,可以解决此问题。 async在HTML解析期间下载文件,并在完成下载后暂停HTML解析器执行它。 defer在HTML解析期间下载文件,并且只在解析器完成后才执行它。

    推测解析

    有些浏览器旨在通过使用所谓的推测性解析来解决<script>的阻塞问题。在下载和执行脚本时,引擎会向前解析(并运行HTML树构造!)。 Firefox和Chrome使用这种技术。

    如果推测成功,您可以想象性能提升(例如,DOM未被文档中包含的脚本更改)。等待脚本执行不是必需的,页面已成功绘制。 不利的一面是,当投机失败时,工作量会减少。

    幸运的是,非常聪明的人会使用这些技术,所以即使正确使用document.write也不会破坏这个过程。另一个经验法则是不使用document.write。例如,它可能会破坏推测树:

    // Results in an unbalanced tree
    <script>document.write("<div>");</script>
    
    // Results in an unfinished token
    <script>document.write("<div></div");</script>
    

    进一步阅读

    以下资源值得您花时间阅读:

答案 1 :(得分:2)

  

另一种说法是,将<script>放在<body>的末尾是最佳做法,以便在下载脚本之前页面呈现内容。

将脚本标记放在body标记末尾的主要原因是:下载执行 JavaScripts将阻止HTML解析(或者,您可以说它们只是部分解析)。如果将它们放入<head>,则用户可能需要等待很长时间才能在网页上看到任何内容。你有一个像这样的html页面的图像:

<html>
  <head>
    <!-- this huge.js takes 10 seconds to download -->
    <script src="huge.js"></script>
  </head>
  <body>
    <div>
      My most fancy div!
    </div>
  </body>
</html>

// huge.js
(function () {
   // Some CPU intensive JS operations which take 10 second to complete
})();

浏览器将在到达<script>标记后立即开始执行这些CPU密集型JS。它将阻止解析其余的HTML内容。因此,在这种情况下,用户在下载并执行JavaScript之前无法看到他的花哨div(总共需要20秒)。

您可以使用DOMContentLoaded来检测是否加载并解析了初始DOM。你在上一段中的陈述非常正确:每次HTML解析器看到<script>时,它都会同步下载并执行它(参见注意2 )。在执行所有<script>并解析所有HTML后,将触发DOMContentLoaded

通知1 DOMContentLoaded不会等待CSS和图片

通知2 :大多数浏览器都有&#34;推测解析&#34;特征。如果有多个JavaScript文件,则会同时下载它们。但是,它们仍将由主线程顺序执行。

关于你的上一个问题:

  

或者,页面是否在解析DOM树的同时绘制页面?

根据我自己的理解,答案是,浏览器会尝试画出 ASAP 。也就是说,绘制引擎不会等待渲染树完全准备好。所以应该有一个分离的线程来处理油漆。

如果我的理解有误,请随时纠正我:)

参考文献:

  1. https://www.chromium.org/developers/the-rendering-critical-path
  2. http://taligarsiel.com/Projects/howbrowserswork1.htm

答案 2 :(得分:0)

这实际上取决于浏览器关于加载所有内容的特定顺序,但对于DOM解析,它从上到下工作。解析器逐分支移动,因此当遇到头部时,它将遍历每个子节点。如果一个元素有一个子元素,它将移动到子/子上,然后再向上移动树。把它放在非常基本的伪代码中: 而DOM!=解析:     if current_node.has_child():         current_node = child_node         execute_node()     elif current_node.has_sibling():         current_node = sibling_node         execute_node()     elif current_node.has_parent_sibling():         current_node = parent_sibling         execute_node()     其他:         current_node = parent_node 它基本上将脚本/链接标记作为父节点处理,如果它是外部文件则启动HTTP / S GET请求,并在移动到下一个节点之前解析代码。所以我们在最后使用put脚本标签的原因是因为它们通常不会在页面加载时使用,而是在加载后处理。所以大家一致认为,最好在页面上获取一些东西,然后再加载你的JS,以便它可以处理你在菜单项上那么重要的动画。 当然有例外,你可以指定DOM解析器异步执行脚本 - 解析器创建一个额外的线程来解析JS - 或者延迟 - 发出GET请求,但是直到HTML文档才解析文件完成解析。