渲染进程的内部机制这是关于浏览器工作原理博客系列四部分中的第三部分。之前,我们介绍了多进程架构和导航流。在这篇文章中,我们将一探渲染进程的内部机制。
渲染进程涉及 Web 性能的许多方面。由于渲染进程的流程太复杂,因此本文只进行概述。如果你想深入了解,可以在 the Performance section of Web Fundamentals 找到相关资源。 渲染进程处理网站内容渲染进程负责标签页内发生的所有事情。在渲染进程中,主线程处理服务器发送到用户的大部分代码。如果你使用 web worker 或 service worker,部分 JavaScript 将由工作线程处理。合成和光栅线程也在渲染进程内运行,以高效,流畅地呈现页面。
渲染进程的核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。
图 1:渲染进程内部包含主线程、工作线程、合成线程和光栅线程 解析(Parsing)DOM 的构建当渲染进程收到导航的提交消息并开始接收 HTML 数据时,主线程开始解析文本字符串(HTML)并将其转换为文档对象模型(DOM)。
DOM 是一个页面在浏览器内部表现,也是 Web 开发人员可以通过 JavaScript 与之交互的数据结构和 API。
将 HTML 到 DOM 的解析由 HTML Standard 规定。你可能已经注意到,将 HTML 提供给浏览器这一过程从不会引发错误。像 Hi! <b>I'm <i>Chrome</b>!</i> 这样的错误标记,会被理解为 Hi! <b>I'm <i>Chrome</i></b><i>!</i>,这是因为 HTML 规范会优雅地处理这些错误。如果你好奇这是如何做到的,可以阅读 An introduction to error handling and strange cases in the parser 的 HTML 规范部分。 子资源加载网站通常使用图像、CSS 和 JavaScript 等外部资源,这些文件需要从网络或缓存加载。在解析构建 DOM 时,主线程会按处理顺序逐个请求它们,但为了加快速度,“预加载扫描器(preload scanner)”会同时运行。如果 HTML 文档中有 <img> 或 <link> 之类的内容,则预加载扫描器会查看由 HTML 解析器生成的标记,并在浏览器进程中向网络线程发送请求。
图 2:主线程解析 HTML 并构建 DOM 树 JavaScript 阻塞解析当 HTML 解析器遇到 <script> 标记时,会暂停解析 HTML 文档,开始加载、解析并执行 JavaScript 代码。为什么?因为JavaScript 可以使用诸如 document.write() 的方法来改写文档,这会改变整个 DOM 结构(HTML 规范里的 overview of the parsing model 中有一张不错的图片)。这就是 HTML 解析器必须等待 JavaScript 运行后再继续解析 HTML 文档原因。如果你对 JavaScript 执行中发生的事情感到好奇,可以看看 V8 团队就此发表的演讲和博客文章。 提示浏览器如何加载资源Web 开发者可以通过多种方式向浏览器发送提示,以便很好地加载资源。如果你的 JavaScript 不使用 document.write(),你可以在 <script> 标签添加 async 或 defer 属性,这样浏览器会异步加载运行 JavaScript 代码,而不阻塞解析。如果合适,你也可以使用 JavaScript 模块。可以使用 <link rel="preload"> 告知浏览器当前导航肯定需要该资源,并且你希望尽快下载。有关详细信息请参阅 Resource Prioritization – Getting the Browser to Help You。 样式计算只拥有 DOM 不足以确定页面的外观,因为我们会在 CSS 中设置页面元素的样式。主线程解析 CSS 并确定每个 DOM 节点计算后的样式。这是有关基于 CSS 选择器对每个元素应用何种样式的信息,这可以在 DevTools 的 computed 部分中看到。
图 4:一个人站在一幅画前,电话线与另一个人相连
布局是计算元素几何形状的过程。主线程遍历 DOM,计算样式并创建布局树,其中包含 x y 坐标和边界框大小等信息。布局树可能与 DOM 树结构类似,但它仅包含页面上可见内容相关的信息。如果一个元素应用了 display:none,那么该元素不是布局树的一部分(但 visibility:hidden 的元素在布局树中)。类似地,如果应用了如 p::before{content:"Hi!"} 的伪类,则即使它不在 DOM 中,也包含于布局树中。