HTML/CSS/JS的解析渲染

1.渲染器进程的内部工作原理

本系列分为 4 个部分,主要讲解关于现代浏览器的运行原理,本文为该系列的第 3 篇。在之前的文章中,我们介绍了多进程架构导航的完整流程,而在这篇文章中,我们将探究在渲染器进程的内部,到底发生了什么。渲染器进程涉及到 Web 性能相关的多个方面,由于渲染器进程中处理了很多的逻辑,不是一篇文章可以全面讲解的,因此本文仅作为一个概述。如果你有兴趣深入研究,可以在《Why Performance Matters》这篇文章里找到更多的资料。

2.渲染器进程处理Web内容

所有选项卡内发生的逻辑,都由渲染器进程负责。在渲染器进程中,主线程处理了服务器发送给用户的大部分代码。如果你使用到 Web Workder 或者Service Worker,那 JavaScript 中的这部分代码,将由工作线程处理。Compositor(合成器) 和 Raster(光栅) 线程也在渲染器内运行,从而实现高效、流畅的渲染页面。渲染器进程的核心工作是将 HTML,CSS 和 JavaScript 转换为用户可以与之交互的网页。


上图中,描述了具有主线程、工作线程、Compositor 线程、Raster 线程的渲染器进程,以及他们之间的关系


3.解析

构建 DOM:
当渲染器进程收到一个导航请求,并开始接收 HTML 数据,主线程将开始处理文本字符串(HTML),将其解析成 DOM(Document Object Model)。

DOM 是 Web 页面的内部的逻辑树文档结构,Web 开发人员可以通过 JavaScript 脚本与之交互数据,以及通过标准 API 来操作 DOM 节点。

将 HTML 文档解析成 DOM 是完全依照于 HTML 协议。并且在 HTML 协议中,浏览器不会对错误的 HTML 进行错误提示。例如,缺少结束的 </p> 标签时,这依然是一个有效的 HTML。类似 Hi! <b>I'm <i>Chrome</b>!</i> 中,b 标签在 i 标签之前关闭这样的错误,会被 HTML 理解为 Hi! <b>I'm <i>Chrome</i></b><i>!</i>。这是因为 HTML 规范的主要原则是优雅的处理这些错误,而不是严格检查。

子资源加载:

一个完整的 Web 站点通常会包含图片、CSS 和 JS 等外部资源,这些文件都需要从网络或者本地缓存中加载。主线程可以在解析构建 DOM 的时候,将他们逐个请求,但是为了加快速度,会同时使用 “预加载扫描(Preload Scanner)”。

如果 “预加载扫描” 发现有类似 <img> 或 <link> 这样的标签时,会由 HTML 解析器对该资源生成一个 Tokens,然后在浏览器进程中,通过网络或者本地缓存来加载资源。

上图描述了,主线程解析 HTML 并构建 DOM 树的过程。

JS 可以阻止解析

当 HTML 解析器遇到 <script> 标签的时候,它会暂停解析 HTML 文档,然后对这个 JS 脚本进行加载、解析和执行。这么设计的原因,是因为 JS 可以使用类似 document.write() 方法来改变 DOM 的结构。这就是 HTML 解析器在重新解析 HTML 之前,必须等待 JS 脚本执行的原因。HTML 遇到 JS 脚本则暂停对 HTML 的解析,这并不是绝对的。Web 开发人员可以通过多种方式的配置,告知浏览器如何更优雅的加载资源。如果你的 JS 脚本中,没有使用到类似 document.write() 这样的方法,你可以在 script 标签中添加 async 或 defer 标记,然后浏览器会异步加载和运行此 JS 脚本,不会阻断解析。如果需要,也可以使用 JavaScript Modules,还可以通过 <link rel="preload"> 标签向浏览器明确标记此为重要的资源,将在页面加载完成之后被立刻使用,对于这类资源,它会在页面加载生命周期的早期,被优先加载。

样式渲染(Style)

仅仅解析成 DOM,还不足以完成页面渲染,因为还可以通过在 CSS 中,设置元素的样式来丰富渲染效果。主线程将解析 CSS,并将效果渲染到指定的 DOM 节点上,关于 CSS 选择器如何定位到指定的 DOM 节点,可以通过 DevTools 来查看相关信息。

上图中,主线程解析 CSS 并添加渲染样式。即使你不使用任何 CSS 样式,每个 DOM 节点依然存在默认的渲染样式。例如,h1标签在视觉上就大于 h2 标签,并且每个元素还有默认的边距。这是因为浏览器具有默认样式表。

布局(Layout)

到现在,渲染器进程知道每个 DOM 的结构和样式了,但是这依然不足以渲染页面。想象一下,你正视图通过文字向朋友描述一副画,“有一个大的红色圆圈和一个小的蓝色方块”,这些信息不足以让你的朋友还原这幅画。这就牵扯到布局(Layout),布局是对元素定位的过程,主线程遍历 DOM 并计算样式,然后创建布局树(Layout Tree),在布局树中,包含 X、Y 坐标和边框大小等信息。布局树是一个与 DOM 树类似的结构,但是它仅仅包含了页面上可见内容相关的信息。举个例子,如果某个元素设置了 display:none,则该元素将不会出现在布局树中,但是它会出现在 DOM 树中,而如果该元素被设置为 visibility:hidden 则它会存在于布局树中。类似的例子还有 p::before{content:"Hi!"} 这样的伪类,它会存在于布局树中,而不会存在于 DOM 树中

绘制(Paint)

拥有 DOM、CSS 和 LayoutTree 仍然不足以渲染页面。假设你正在尝试重绘一幅画,你除了需要知道元素的大小、外观和位置之外,还需要知道它们的绘制顺序。例如:z-index 属性将改变元素的层级,在这种情况下,按 HTML 中编写的元素顺序进行绘制,将导致渲染结果和预期不符。在这个绘制的过程中,主线程遍历布局树,然后创建绘制记录。绘制记录是一个绘制过程的注释,例如“背景优先,然后是文本,最后是矩形”。如果你曾经使用 JS 在 <canvas>上绘制元素,那么你对此过程应该会很熟悉。

原文链接:

https://developers.google.com/web/updates/2018/09/inside-browser-part3