繁体   English   中英

何时解析 HTML DOM 树?

[英]When does parsing HTML DOM tree happen?

我总是看到一个网页的渲染流程,如下图所示: 在此处输入图片说明

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

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

TL;DR:收到文件后立即开始解析。

解析和绘制

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

渲染引擎解析 HTML 文档并创建两棵树: content treerender tree 内容树包含所有 DOM 节点。 呈现树包含了所有的造型信息( CSSOM ),只有所需要的渲染TE页面的DOM节点。

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

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

您可以在浏览器中看到这个过程。 例如,打开 Chrome 开发人员工具并加载您选择的站点。

网络选项卡

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

时间轴选项卡

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

单线程

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

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

  1. 必须从网络获取资源(由于 DNS 查找和连接速度,这可能是一个缓慢的过程)。
  2. 资源的内容被传递给 Javascript 解释器。
  3. 解释器解析并执行代码。

解析文档会停止,直到此过程完成。 您不会通过在文档末尾包含<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>

进一步阅读

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

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

将 script 标签放在 body 标签末尾的主要原因是:下载执行JavaScript 会阻止 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 )。 执行DOMContentLoaded所有<script>并解析所有 HTML 后,将触发DOMContentLoaded

注意 1DOMContentLoaded不会等待 CSS 和图像

注意 2 :大多数浏览器都具有“推测解析”功能。 如果有多个 JavaScript 文件,它们将被同时下载。 但是,它们仍然会由主线程顺序执行。

对于您的最后一个问题:

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

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

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

参考:

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

这实际上取决于浏览器加载所有内容的特定顺序,但对于 DOM 解析,它从上到下工作。 解析器逐个分支地移动,因此当它遇到头部时,它会遍历每个子节点。 如果一个元素有一个子元素,它将在移回树之前移动到子元素/子元素上。 把它放在非常基本的伪代码中:

while DOM != parsed:
    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()
    else:
        current_node = parent_node

它本质上将脚本/链接标签作为父节点处理,如果它是外部文件,则启动 HTTP/S GET 请求,并在移动到下一个节点之前解析代码。 所以我们在最后使用 put script 标签的原因是因为它们通常不在页面加载时使用,而是在加载后处理。 所以大家的共识是,最好在页面上获取一些内容,然后稍后加载您的 JS,以便它可以处理您在菜单项上拥有的如此重要的动画。

当然也有例外,您可以指定 DOM 解析器以异步方式执行脚本 - 解析器创建一个额外的线程来解析 JS - 或延迟 - 发出 GET 请求但该文件直到 HTML 文档被解析解析完毕。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM