深入了解现代浏览器(第三部分)
作者:互联网
渲染器进程的内部工作
这是 4 部分博客系列中的第 3 部分,着眼于浏览器的工作原理。 之前,我们介绍了多进程架构和导航流程。 在这篇文章中,我们将看看渲染器进程内部发生了什么。
渲染器过程涉及 Web 性能的许多方面。 由于渲染器进程内部发生了很多事情,所以这篇文章只是一个概述。 如果您想深入挖掘,Web Fundamentals 的性能部分有更多资源。
渲染器进程处理 Web 内容
渲染器进程负责选项卡内发生的所有事情。 在渲染器进程中,主线程处理您的需要呈现给用户的页面的大部分代码。 如果您使用 web worker 或 service worker,有时部分 JavaScript 由工作线程处理。 合成器(Compositor )和光栅线程( raster threads)也在渲染器进程内部运行,以高效、流畅地渲染页面。
渲染器进程的核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。
(Figure 1: Renderer process with a main thread, worker threads, a compositor thread, and a raster thread inside)
解析
DOM的构建
当渲染器进程收到导航的提交消息并开始接收 HTML 数据时,主线程开始解析文本字符串 (HTML) 并将其转换为文档对象模型 (DOM)。
DOM 是浏览器对页面的内部表示,也是 Web 开发人员可以通过 JavaScript 与之交互的数据结构和 API。
将 HTML 文档解析为 DOM 由 HTML 标准定义。 您可能已经注意到,将 HTML 提供给浏览器永远不会引发错误。 例如,缺少结束
标记是有效的 HTML。 像Hi! <b>I'm <i>Chrome</b>!</i>
这样的错误标记 (b 标签在 i 标签之前关闭)被视为您写了Hi! <b>I'm <i>Chrome</i></b><i>!
。 这是因为 HTML 规范旨在优雅地处理这些错误。 如果您对这些事情是如何完成的感到好奇,您可以阅读 HTML 规范的“解析器中的错误处理和奇怪案例简介”部分。
子资源加载
网站通常使用图像、CSS 和 JavaScript 等外部资源。 这些文件需要从网络或缓存中加载。 主线程可以在解析构建DOM的过程中找到它们时一一请求,但为了加快速度,“预加载扫描器(preload scanner)”是并发运行的。 如果 HTML 文档中有 或 之类的东西,预加载扫描器会查看 HTML 解析器生成的令牌(tokens ),并向浏览器进程中的网络线程发送请求。
(Figure 2: The main thread parsing HTML and building a DOM tree)
JavaScript 可以阻止解析
当 HTML 解析器找到 <script>
标记时,它会暂停 HTML 文档的解析,并且必须加载、解析和执行 JavaScript 代码。 为什么? 因为 JavaScript 可以使用诸如 document.write() 之类的东西来改变文档的形状,这会改变整个 DOM 结构(HTML 规范中的解析模型概述有一个很好的图表)。 这就是为什么 HTML 解析器必须等待 JavaScript 运行才能恢复对 HTML 文档的解析。 如果您对 JavaScript 执行过程中发生的事情感到好奇,V8 团队对此进行了讨论和博客文章。
提示浏览器如何加载资源
Web 开发人员可以通过多种方式向浏览器发送提示,以便更好地加载资源。 如果您的 JavaScript 不使用 document.write()
,您可以将 async
或defer
属性添加到 <script>
标记。 然后浏览器异步加载和运行 JavaScript 代码,并且不会阻止解析。 如果合适的话,你也可以使用JavaScript module 。 是一种通知浏览器当前导航肯定需要该资源并且您希望尽快下载的方法。 您可以在 Resource Prioritization – Getting the Browser to Help You中阅读更多信息。
样式计算
拥有 DOM 不足以知道页面的外观,因为我们可以在 CSS 中设置页面元素的样式。 主线程解析 CSS 并确定每个 DOM 节点的计算样式。 这是关于基于 CSS 选择器将哪种样式应用于每个元素的信息。 您可以在 DevTools 的computed
部分中查看此信息。
(Figure 3: The main thread parsing CSS to add computed style)
即使您不提供任何 CSS,每个 DOM 节点也有一个计算样式。
标签显示得比
标签大,并且为每个元素定义了边距。 这是因为浏览器有一个默认样式表。 如果你想知道 Chrome 的默认 CSS 是怎样的,可以在这里查看源代码。
布局
现在渲染器进程知道文档的结构和每个节点的样式,但这还不足以渲染页面。 想象一下,您正试图通过电话向您的朋友描述一幅画。 “有一个大红色圆圈和一个小蓝色方块”不足以让您的朋友知道这幅画的确切外观。
(Figure 4: A person standing in front of a painting, phone line connected to the other person)
布局是查找元素几何形状的过程。 主线程遍历 DOM 和计算样式,并创建包含 x y 坐标和边界框大小等信息的布局树。 布局树可能类似于 DOM 树的结构,但它只包含与页面上可见内容相关的信息。 如果 display: none
被应用,则该元素不是布局树的一部分(但是,具有visibility: hidden
的元素则位于布局树中)。 类似地,如果应用了具有类似 p::before{content:"Hi!"}
内容的伪类,它会包含在布局树中,即使它不在 DOM 中。
(图 5:主线程使用计算样式遍历 DOM 树并生成布局树)
确定页面的布局是一项具有挑战性的任务。 即使是最简单的页面布局,比如从上到下的块流,也必须考虑字体有多大以及在哪里换行,因为这些会影响段落的大小和形状; 然后影响下一段需要的位置。
CSS 可以使元素浮动到一侧,屏蔽溢出项,改变书写方向。 可以想象,这个布局阶段任务艰巨。 在 Chrome 中,整个工程师团队都在处理布局。 如果你想了解他们的工作细节,BlinkOn 会议上的演讲被记录了下来,非常有趣。
画页面
拥有 DOM、样式和布局仍然不足以呈现页面。 假设您正在尝试复制一幅画。 您知道元素的大小、形状和位置,但您仍然需要判断绘制它们的顺序。
(Figure 7: A person in front of a canvas holding paintbrush, wondering if they should draw a circle first or square first)
例如,可能会为某些元素设置 z-index,在这种情况下,按照 HTML 中编写的元素的顺序绘制将导致不正确的呈现。
(图 8:页面元素按 HTML 标记的顺序出现,由于没有考虑 z-index,导致呈现错误的图像)
在此绘制步骤中,主线程遍历布局树以创建绘制记录。 绘画记录是“先背景,后文字,后矩形”的绘画过程的记录。 如果您使用 JavaScript 在 <canvas>
元素上绘图,您可能对这个过程很熟悉。
(图 9:主线程遍历布局树并生成绘制记录)
更新渲染管道成本高昂
Figure 10: DOM+Style, Layout, and Paint trees in order it is generated
在渲染管道中要掌握的最重要的事情是,在每个步骤中,前一个操作的结果都用于创建新数据。 例如,如果布局树发生变化,则需要为文档的受影响部分重新生成绘制顺序。
如果您正在为元素制作动画,则浏览器必须在每一帧之间运行这些操作。 我们的大多数显示器每秒刷新屏幕 60 次 (60 fps); 当您在每一帧都在屏幕上移动物体时,动画对人眼来说会显得很流畅。 但是,如果动画错过了中间的帧,那么页面将出现卡顿。
(Figure 11: Animation frames on a timeline)
即使你的渲染操作能够跟上屏幕刷新,这些计算也在主线程上运行,这意味着当你的应用程序运行 JavaScript 时它可能会被阻塞。
(图 12:时间轴上的动画帧,但一帧被 JavaScript 阻止)
您可以使用requestAnimationFrame()
将 JavaScript 操作分成小块并安排在每一帧运行。 有关此主题的更多信息,请参阅优化 JavaScript 执行。 您也可以在 Web Workers 中运行 JavaScript 以避免阻塞主线程。
(图 13:在带有动画帧的时间轴上运行的较小的 JavaScript 块)
合成
你将如何绘制页面?
既然浏览器知道了文档的结构、每个元素的样式、页面的几何形状以及绘制顺序,那么它是如何绘制页面的呢? 将此信息转换为屏幕上的像素称为光栅化(rasterizing)。
处理这个问题的一种天真的方法可能是在视口内对部分进行光栅化。 如果用户滚动页面,则移动光栅框架,并通过光栅更多来填充缺失的部分。 这就是 Chrome 在首次发布时处理光栅化的方式。 然而,现代浏览器运行一个更复杂的过程,称为合成。
什么是合成
合成是一种将页面的各个部分分成图层、分别光栅化它们并在称为合成器线程的单独线程中合成为页面的技术。 如果发生滚动,由于图层已经被光栅化,它所要做的就是合成一个新的帧。 可以通过移动图层并合成新帧以相同的方式实现动画。
您可以使用“Layers”面板在 DevTools 中查看您的网站是如何划分层的。
分层
为了找出哪些元素需要在哪些层中,主线程遍历布局树来创建分层树(这部分在 DevTools 性能面板中称为“更新层树,Update Layer Tree”)。 如果页面的某些部分应该是单独的层(如滑入式侧边菜单),但是没有得到一个layer,那么您可以通过使用 CSS 中的 will-change 属性来提示浏览器。
(图 16:主线程遍历布局树生成层树)
您可能很想为每个元素赋予层,但是跨过多层的合成可能会导致比每帧光栅化页面的小部分更慢的操作,因此测量应用程序的渲染性能至关重要。 有关该主题的更多信息,请参阅Stick to Compositor-Only Properties and Manage Layer Count。
主线程的光栅和合成
一旦创建了图层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。 合成器线程然后光栅化每一层。 一个层可能像一页的整个长度一样大,因此合成器线程将它们分成小块并将每个小块发送到光栅线程。 光栅线程光栅化每个图块并将它们存储在 GPU 内存中。
(图 17:光栅线程创建图块位图并发送到 GPU)
合成器线程可以优先考虑不同的光栅线程,以便可以首先对视口(或附近)内的事物进行光栅化。 一个图层还具有针对不同分辨率的多个平铺,以处理诸如放大操作之类的事情。
一旦图块(tiles)被光栅化,合成器线程收集称为绘制四边形的图块信息以创建合成器框架。
绘制四边形 | 包含了内存中的图块位置信息,以及在考虑到页面合成的情况下,可以将图块绘制到页面的哪个位置的信息 |
---|---|
合成器架构 | 绘制代表页面框架的四边形的draw的集合 |
然后通过 IPC 将合成器框架提交给浏览器进程。 此时,可以从 UI 线程添加另一个合成器框架以更改浏览器 UI,或从其他渲染器进程添加用于扩展。 这些合成器帧被发送到 GPU 以在屏幕上显示。 如果出现滚动事件,合成器线程会创建另一个合成器帧以发送到 GPU。
(图 18:合成器线程创建合成帧。 帧被发送到浏览器进程,然后发送到 GPU)
合成的好处是它是在不涉及主线程的情况下完成的。 合成器线程不需要等待样式计算或 JavaScript 执行。 这就是为什么只合成动画被认为是流畅性能的最佳选择。 如果需要重新计算布局或绘制,则必须涉及主线程。
总结
在这篇文章中,我们研究了从解析到合成的渲染管道。 希望您现在能够阅读有关网站性能优化的更多信息。
在本系列的下一篇也是最后一篇文章中,我们将更详细地了解合成器线程,并了解当鼠标移动和点击等用户输入进入时会发生什么。
你喜欢这篇文章吗? 如果您对未来的帖子有任何问题或建议,我很乐意在下面的评论部分或 Twitter 上的@kosamari 收到您的来信。
Next: 输入即将进入合成器
标签:合成器,浏览器,DOM,JavaScript,第三,HTML,深入,线程,页面 来源: https://www.cnblogs.com/lhjc/p/16441070.html