原文链接 developers.google.com/web/updates…
渲染器进程的内部工作原理
这是关于浏览器如何工作的系列文章的第 3 篇。 之前,我们介绍了 多进程架构 和 导航流。 在这篇文章中,我们将看看渲染器进程内部发生了什么。
渲染器过程涉及 Web 性能的许多方面。 由于渲染器进程内部发生了很多事情,这篇文章只是一个概括性的概述。 如果您想深入挖掘,Why does speed matter? 关于性能部分有更多的资源。
渲染器进程处理 Web 内容
渲染器进程负责选项卡内发生的所有事情。 在渲染器进程中,主线程处理您发送给用户的大部分代码。如果您使用 Web Worker 或 Service Worker,有时部分 JavaScript 会由 worker 线程处理。为了高效流畅地渲染页面,合成器和光栅线程也在渲染器进程中运行。
渲染器进程的核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。
解析
DOM的构建
当渲染器进程收到导航的提交消息并开始接收 HTML 数据时,主线程开始解析文本字符串 (HTML) 并将其转换为文档对象模型 (DOM)。
DOM 是浏览器对页面的内部表示,也是 Web 开发人员可以通过 JavaScript 与之交互的数据结构和 API。
将 HTML 文档解析为 DOM 是由 HTML 标准 定义的。您可能已经注意到,向浏览器提供 HTML 永远不会引发错误。例如:缺少结束 </p> 标签是有效的 HTML,像Hi! <b>I'm <i>Chrome</b>!</i>(b 标签在 i 标签之前关闭)错误标记被视为你写的Hi! <b>I'm <i>Chrome</i></b><i>!</i>,这是因为 HTML 规范旨在优雅地处理这些错误。如果你好奇这些事情是如何完成的,您可以阅读 HTML 规范的 An introduction to error handling and strange cases in the parser 部分。
子资源加载
网站通常使用图像、CSS 和 JavaScript 等外部资源。 这些文件需要从网络或缓存加载。 主线程可以在解析构建 DOM 的过程中找到它们后一一请求它们,但为了加快速度,“预加载扫描器”是同时运行的。如果 HTML 文档中存在 <img> 或 <link> 之类的内容,则预加载扫描器会查看 HTML 解析器生成的 tokens,并将请求发送到浏览器进程中的网络线程。
JavaScript 会阻止解析
当 HTML 解析器找到 <script> 标签时,它会暂停 HTML 文档的解析,去加载、解析和执行 JavaScript 代码。 为什么? 因为 JavaScript 可以使用诸如 document.write() 之类的东西,这会改变整个 DOM 结构(详细内容请参阅HTML 规范中的 overview of the parsing model)。这就是 HTML 解析器必须等待 JavaScript 运行才能继续解析 HTML 文档的原因。 如果您对 JavaScript 执行中发生的事情感到好奇,请参阅V8 团队的 JavaScript engine fundamentals。
提示浏览器您希望如何加载资源
Web 开发人员可以通过多种方式向浏览器发送提示以很好地加载资源。 如果您的 JavaScript 不使用 document.write(),您可以向 <script> 标签添加 async 或 defer 属性。 然后浏览器异步加载和运行 JavaScript 代码,不会阻止解析。 如果合适,您也可以使用 JavaScript modules。 <link rel="preload"> 是一种通知浏览器当前导航肯定需要该资源并且您希望尽快下载的方式。更多相关信息请参阅 Resource Prioritization – Getting the Browser to Help You。
Style 计算
拥有 DOM 并不足以知道页面会是什么样子,因为我们可以在 CSS 中设置页面元素的样式。 主线程解析 CSS 并确定每个 DOM 节点的样式。 这是关于基于 CSS 选择器将哪种样式应用于每个元素的信息。 您可以在 DevTools 的计算部分看到此信息。
即使您不提供任何 CSS,每个 DOM 节点都有一个计算样式。 <h1> 标签显示得比 <h2> 标签大,并且为每个元素定义了边距。 这是因为浏览器有一个默认的样式表。 如果你想知道 Chrome 的默认 CSS 是什么样的,你可以在这里查看 源代码。
布局
现在渲染器进程知道文档的结构和每个节点的样式,但这还不足以渲染页面。 想象一下,您正试图通过电话向您的朋友描述一幅画。 “有一个大的红色圆圈和一个小的蓝色方块”不足以让您的朋友知道这幅画究竟是什么样子。
布局是一个寻找元素几何形状的过程。 主线程遍历 DOM 和计算样式,并创建布局树,其中包含 xy 坐标和边界框大小等信息。 布局树可能与 DOM 树的结构相似,但它只包含与页面上可见的内容相关的信息。 如果用了 display: none,该元素不是布局树的一部分(但是,一个具有visibility: hidden的元素在布局树中)。如果用了像 p::before{content:"Hi!"} 的伪类,即使它不在 DOM 中,它也会包含在布局树中。
确定页面的布局是一项具有挑战性的任务。 即使是最简单的页面布局,比如从上到下的块流,也必须考虑字体有多大以及在哪里换行,因为这些会影响段落的大小和形状; 这会影响下一段需要在哪里。
CSS 可以使元素浮动到一侧,屏蔽溢出项,并改变书写方向。 可想而知,这个布局阶段任务艰巨。 在 Chrome 中,有个工程师团队负责布局。如果您想查看他们工作的详细信息,BlinkOn Conference 的一些 演讲 被记录下来并且非常有趣。
绘制
拥有 DOM、样式和布局仍然不足以渲染页面。 假设您正在尝试复制一幅画。 你知道元素的大小、形状和位置,但你仍然需要判断你绘制它们的顺序。
例如,可能会为某些元素设置 z-index,在这种情况下,按照 HTML 中编写的元素顺序绘制将导致渲染不正确。
在此绘制步骤中,主线程遍历布局树以创建绘制记录。 绘画记录是对“先背景,后文字,再矩形”的绘画过程的记录。 如果您使用 JavaScript 在 <canvas> 元素上绘图,那么您可能熟悉这个过程。
更新渲染 pipeline 的成本很高
渲染管线中最重要的一点是,在每一步都使用上一次操作的结果创建新数据。例如:如果布局树中的某些内容发生了变化,则需要为文档的受影响部分重新生成绘制顺序。
如果您正在为元素设置动画,则浏览器必须在每一帧之间运行这些操作。 我们的大多数显示器每秒刷新屏幕 60 次 (60 fps); 当您在每一帧在屏幕上移动物体时,动画对人眼来说会显得平滑。 但是,如果动画错过了中间的帧,则页面将出现“janky”。
即使您的渲染操作跟上屏幕刷新,这些计算也在主线程上运行,这意味着当您的应用程序运行 JavaScript 时,它可能会被阻止。
您可以将 JavaScript 操作分成小块,并使用 requestAnimationFrame() 安排在每一帧运行。 有关此主题的更多信息,请参阅 Optimize JavaScript Execution。 您还可以在 Web Workers 中运行 JavaScript 以避免阻塞主线程。
合成
你会怎么画一个页面?
既然浏览器知道了文档的结构、每个元素的样式、页面的几何形状和绘制顺序,它如何绘制页面? 将此信息转换为屏幕上的像素称为光栅化。
也许处理这个问题的一种天真的方法是在视口内对部分进行光栅化。 如果用户滚动页面,则移动光栅框架,并通过更多光栅填充缺失的部分。 这就是 Chrome 在首次发布处理光栅化时的方式。然而,现代浏览器运行一个更复杂的过程,称为合成(compositing)。
什么是合成?
合成是一种将页面的各个部分分成多个层、单独光栅化它们并在称为合成器线程的单独线程中合成为一个页面的技术。 如果发生滚动,因为图层已经被光栅化,它所要做的就是合成一个新的帧。 可以通过移动图层并合成新帧以相同的方式实现动画。
您可以使用 Layers panel 在 DevTools 中查看您的网站如何划分为多个图层。
分层
为了找出哪些元素需要在哪些层中,主线程遍历布局树以创建层树(这部分在 DevTools 性能面板中称为“更新层树”)。 如果页面的某些部分应该是单独的层(如滑入式侧边菜单)没有得到,那么您可以通过在 CSS 中使用 will-change 属性来提示浏览器。
CSS 属性 will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。
您可能想为每个元素提供图层,但与每帧光栅化页面的小部分相比,在过多的图层上进行合成可能会导致操作更慢,因此测量应用程序的渲染性能至关重要。更多详细信息请参阅 Stick to Compositor-Only Properties and Manage Layer Count。
主线程的光栅和合成
一旦创建了层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。 合成线程然后光栅化每一层。 一个图层可能像页面的整个长度一样大,因此合成器线程将它们分成多个图块并将每个图块发送到光栅线程。 光栅线程光栅化每个瓦片并将它们存储在 GPU 内存中。
合成器线程可以对不同的光栅线程进行优先级排序,以便可以首先对视口内(或附近)的事物进行光栅化。 一个图层也有多个不同分辨率的平铺来处理诸如放大操作之类的事情。
对切片进行光栅化后,合成器线程会收集称为绘制四边形的切片信息以创建合成器框架。
| 绘制四边形 | 包含诸如磁贴在内存中的位置以及在考虑页面合成的情况下在页面中绘制磁贴的位置等信息。 |
|---|---|
| 合成器框架 | 代表页面框架的绘制四边形的集合。 |
通过 IPC 将合成器框架提交给浏览器进程。 此时,可以从用于浏览器 UI 更改的 UI 线程添加另一个合成器框架,或者从用来扩展的其他渲染器进程添加。这些合成器帧被发送到 GPU 以将其显示在屏幕上。 如果出现滚动事件,合成器线程会创建另一个合成器帧发送到 GPU。
合成的好处是它是在不涉及主线程的情况下完成的。 合成器线程不需要等待样式计算或 JavaScript 执行。 这就是为什么只合成动画被认为是获得流畅性能的最佳选择。 如果需要重新计算布局或绘制,则必须涉及主线程。
总结
在这篇文章中,我们研究了从解析到合成的渲染管道。 希望您能阅读有关网站性能优化的更多信息。
在本系列的下一篇也是最后一篇文章中,我们将更详细地研究合成器线程,看看当鼠标移动和点击等用户输入进来时会发生什么。
文章中涉及到的链接有的需要翻墙,如果有需要翻译的,可以留言给我~