解析HTML DOM树是什么时候进行的?

34
我总是看到一个类似下面图片展示的网页渲染流程: enter image description here 因此,只有在解析DOM树和创建CSSOM之后才开始绘制,对吗?另一种说法是,将<script>放在<body>的末尾是最佳实践,这样页面就能在脚本下载之前呈现出来。
我的问题是,DOM树解析何时发生,我们如何知道它已经完成?在我看来,<script>也是DOM树的一部分,只有当脚本加载完成时,我们才能说DOM树已创建。浏览器从上到下读取html文件,创建DOM树,当它看到<script>时,停止下载并执行它,直到解析整个页面。或者说,页面在解析DOM树的同时进行绘制吗?

2
离题: 很有趣的是看到像这样一个(质量好的)问题接收到了一次踩。这证明了在stackoverflow中仍然存在着巨魔。@JasmineOT你的问题很好! - Marcos Pérez Gude
@MarcosPérezGude 非常感谢您,Marcos。这件事情困扰我很长一段时间了。 - JasmineOT
1
我不得不翻阅我的浏览器历史记录:我使用了这篇文章来理解它。它还指出(见主要流程部分)渲染引擎将开始从网络层获取所请求文档的内容。这通常会以8KB的块进行。 - KarelG
@JasmineOT:这个图形仅显示了单个实体的流程,从资源到出现在屏幕上。它不涉及更新,例如网络输出下一个要解析的块或DOM通过JS进行修改。您必须想象这个流程发生多次,甚至是持续不断的。 - Bergi
网页 https://jackgiffin.com/main/books/Starnes-The-Practice-of-Statistics-AP-5e.html 是一个完美的演示:即使它是一个69MB的网页,通过在HTML中撒入内联<script>元素,它仍然可以相对快速地呈现给用户,以便在您所在的页面下载完成后,它会在整个网页加载完成之前呈现给用户。令人惊讶的是,我测试过的所有浏览器都在整个文件下载完成之前显示了该网页:Chrome、Firefox、Safari、Edge、Opera Mini,甚至是Internet Explorer 11。 - Jack G
3个回答

20
TL;DR:接收文档后立即开始解析。

解析和绘制

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

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

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

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

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

Network tab

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

Timeline tab

记录时间轴可以更深入地了解底层发生的情况。我已经包含了上面的截图作为示例,以表明在解析文档时绘制正在发生。请注意,初始绘制发生在它继续解析文档的另一部分之前。这个过程会持续进行,直到到达文档的结尾。
单线程渲染引擎。除了网络操作外,几乎所有操作都在此线程中完成。
将其与Web的同步特性相结合。开发人员期望脚本会立即被解析和执行(也就是说,一旦解析器到达脚本标记)。这意味着:
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>

更多阅读

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


2
另一种说法是将 <script> 放在 <body> 结束标签处是最佳实践,这样页面可以在脚本下载前渲染出一些内容。
将脚本标签放置于 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(总共需要 20 秒)之前无法看到他的花哨的 div。
您可以使用 DOMContentLoaded 来检测初始 DOM 是否已加载和解析。并且您在上一段中的陈述是相当正确的:每次 HTML 解析器看到一个 <script>,它都会同步下载和执行它(见注意 2)。在所有 <script> 都被执行并且所有 HTML 被解析之后,DOMContentLoaded 将被触发。 注意 1DOMContentLoaded 不会等待 CSS 和图像。 注意 2:大多数浏览器都有“推测解析”功能。如果有多个 JavaScript 文件,则它们将同时下载。但是,它们仍将由主线程按顺序执行。
关于您最后一个问题:

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

根据我的理解,答案是是的,浏览器将尝试尽快绘制。也就是说,绘画引擎不会等待渲染树完全准备好。因此应该有一个单独的线程来处理绘画。
如果我理解有误,请随时纠正我 :)
参考资料:
  1. https://www.chromium.org/developers/the-rendering-critical-path
  2. http://taligarsiel.com/Projects/howbrowserswork1.htm

1
这实际上取决于浏览器加载所有内容的具体顺序,但对于DOM解析,它按自上而下的顺序工作。解析器逐个分支移动,因此当遇到head时,它将通过每个子元素移动。如果一个元素有一个子元素,它将先移动到子元素,然后再返回到树中。用非常基本的伪代码来表达:
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请求并解析代码,然后继续移动到下一个节点。因此,我们将脚本标签放在最后的原因是因为它们通常不会在页面加载时使用,而是在加载后处理事物。因此,共识是最好先在页面上获取某些内容,然后再加载JS,以便处理菜单项上的重要动画效果。当然,也有例外情况,可以指定DOM解析器异步执行脚本-解析JS的额外线程由解析器创建-或延迟-进行GET请求,但在HTML文档完成解析之前不解析文件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接