龙空技术网

深入了解现代浏览器(三)

因特耐特杂货铺 216

前言:

眼前兄弟们对“cssrepaints”大概比较关切,小伙伴们都需要了解一些“cssrepaints”的相关知识。那么小编也在网上收集了一些有关“cssrepaints””的相关资讯,希望朋友们能喜欢,各位老铁们一起来学习一下吧!

  翻译文章,原文:Inside look at modern web browser (part 3)[1]

渲染器进程的内部运作

这是浏览器(共4部分)系列文章的第3部分,介绍了浏览器的工作方式。之前,我们介绍了多进程体系结构和导航流程。在本文中,我们将研究渲染器过程中发生的情况。

渲染器过程涉及Web性能的许多方面。由于渲染器过程内部发生了很多事情,因此本文仅是概述。如果您想更深入地了解,Web基础知识的性能部分[2]将提供更多资源。

渲染器进程处理Web内容

渲染器进程负责选项卡内部发生的所有事情。在渲染器过程中,主线程处理您发送给用户的大多数代码。如果您使用网络工作者或服务工作者,则有时JavaScript的某些部分由工作者线程处理。合成器和光栅线程也运行在渲染器进程内部,以高效,流畅地渲染页面。

渲染器进程的核心工作是将HTML,CSS和JavaScript转换为用户可以与之交互的网页。

图1:内部包含主线程,工作线程,合成器线程和栅格线程的渲染器进程

解析DOM的构造

当渲染器进程收到导航的提交消息并开始接收HTML数据时,主线程开始解析文本字符串(HTML),并将其转换为文档对象模型(DOM)。

DOM是浏览器对页面的内部表示以及Web开发人员可以通过JavaScript与之交互的数据结构和API。

HTML标准将HTML文档解析为DOM。您可能已经注意到,将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[3]"部分。

子资源加载

网站通常使用外部资源,例如图像,CSS和JavaScript。这些文件需要从网络或缓存中加载。主线程可以在解析以构建DOM时找到它们时一个接一个地请求它们,但是为了加快速度,“预加载扫描程序”同时运行。如果HTML文档中有<img>或<link>之类的内容,则预加载扫描程序会窥视HTML解析器生成的令牌,并在浏览器进程中将请求发送到网络线程。

图2:主线程解析HTML并构建DOM树

JavaScript可以阻止解析

HTML解析器找到<script>标记时,它将暂停HTML文档的解析,并且必须加载,解析和执行JavaScript代码。为什么?因为JavaScript可以使用诸如document.write()之类的东西来改变文档的形状,从而改变整个DOM结构(overview of the parsing model in the HTML spec[4]有一个不错的图)。这就是HTML解析器必须等待JavaScript运行才能恢复HTML文档解析的原因。如果您对JavaScript执行中的操作感到好奇,则V8团队对此有相关的文章[5]。

提示浏览器如何加载资源

Web开发人员可以通过多种方式将提示发送到浏览器,以便很好地加载资源。如果您的JavaScript不使用document.write(),则可以向<script>标记添加async或defer属性。然后,浏览器将异步加载并运行JavaScript代码,并且不会阻止解析。如果合适,您也可以使用JavaScript模块[6]。<link rel ="preload">是一种通知浏览器该资源对于当前导航肯定是一种方法,您希望尽快下载。你可以看这些内容Resource Prioritization – Getting the Browser to Help You.[7]

样式处理(Style)

拥有DOM不足以知道页面的外观,因为我们可以在CSS中设置页面元素的样式。主线程解析CSS并确定每个DOM节点的计算样式。这是有关基于CSS选择器将哪种样式应用于每个元素的信息。您可以在DevTools的计算部分中查看此信息。

图3:主线程解析CSS以添加计算样式

布局(Layout)

现在,渲染器进程知道每个节点的文档结构和样式,但这不足以渲染页面。想象一下,您正在尝试通过电话向您的朋友描述一幅画。“有一个大的红色圆圈和一个小小的蓝色方块”不足以让您的朋友知道这幅画的模样。

图4:一个人站在绘画前,电话线与另一个人相连

布局是查找元素几何形状的过程。主线程遍历DOM和计算样式,并创建布局树,该树具有诸如x y坐标和边界框大小之类的信息。布局树的结构可能与DOM树类似,但它仅包含与页面上可见内容有关的信息。如果使用display:none,则该元素不属于布局树(但是,具有可见性:hidden的元素位于布局树中)。同样,如果应用了一个伪类,其内容类似p::before{content:"Hi!"},即使它不在DOM中,它也将包含在布局树中。

图5:主线程遍历具有计算样式的DOM树并生成布局树

确定页面的布局是一项艰巨的任务。即使是最简单的页面布局(如从上到下的块流程)也必须考虑字体的大小以及在何处换行,因为这会影响段落的大小和形状。这会影响下一段的位置。

CSS可以使元素浮动到一侧,掩盖溢出项,并更改书写方向。您可以想象,这个布局阶段有艰巨的任务。在Chrome中,整个工程师团队负责布局。如果您想查看他们的工作的详细信息,few talks from BlinkOn Conference[8]非常有趣。

绘图

图7:一个拿着画笔的画布前面的人,想知道他们应该先画圆形还是正方形

拥有DOM,样式和布局仍然不足以呈现页面。假设您正在尝试复制一幅画。您知道元素的大小,形状和位置,但是仍然需要判断以什么顺序绘制它们。

例如,可能为某些元素设置了z-index,在这种情况下,按HTML中写入的元素顺序进行绘制将导致错误的呈现。

图8:页面元素以HTML标记的顺序出现,导致渲染图像错误,因为未考虑z-index

在此绘制步骤中,主线程遍历布局树以创建绘制记录。绘画记录是对绘画过程的记录,例如“首先是背景,然后是文本,然后是矩形”。如果您使用JavaScript在<canvas>s元素上进行绘制,则此过程可能是您熟悉的。

图9:主线程遍历布局树并生成绘制记录

更新渲染管道的成本很高

在渲染管道中要掌握的最重要的事情是,在每个步骤中,先前操作的结果都用于创建新数据。例如,如果布局树中发生某些更改,则需要为文档的受影响部分重新生成“绘制”顺序。

演示视频[9]

如果要设置动画元素,浏览器必须在每帧之间运行这些操作。我们大多数显示器每秒刷新屏幕60次(60 fps);当您在每一帧中在屏幕上移动物体时,动画将对人眼显得平滑。但是,如果动画错过了它们之间的帧,则页面将显示为“混乱”。

图10:时间线上的动画帧

即使渲染操作与屏幕刷新保持一致,这些计算仍在主线程上运行,这意味着当您的应用程序运行JavaScript时,它可能会被阻止。

图11:时间轴上的动画帧,但JavaScript阻止了一帧

您可以将JavaScript操作分成小块,并使用requestAnimationFrame()安排在每个帧上运行。有关此主题的更多信息,请参见优化JavaScript执行[10]。您也可以在Web Workers中运行JavaScript[11],以避免阻塞主线程。

图12:在带有动画帧的时间轴上运行的JavaScript较小块

合成您将如何绘制页面?

现在,浏览器知道了文档的结构,每个元素的样式,页面的几何形状以及绘制顺序,它如何绘制页面?将此信息转换为屏幕上的像素称为光栅化。

演示视频[12]

处理此问题的一种幼稚方法可能是在视口内部栅格化零件。如果用户滚动页面,则移动栅格化的框架,并通过栅格化更多内容来填充缺少的部分。这是Chrome首次发布时处理栅格化的方式。但是,现代浏览器运行着一种更复杂的过程,称为合成。

什么是合成?

合成是一种将页面的各个部分分成若干层,分别对其进行栅格化并在称为合成器线程的单独线程中作为页面进行合成的技术。如果发生滚动,则因为图层已经被光栅化,所以要做的就是合成一个新的帧。可以通过移动图层并合成新的帧来以相同的方式实现动画。

演示动画[13]

您可以在开发者工具中使用图层面板[14]查看你的网站被划分了多少个图层。

分层

为了找出哪些元素需要位于哪个图层中,主线程遍历了布局树以创建图层树(在开发者性能面板中,此部分称为“Update Layer Tree”——更新图层树)。如果页面的某些部分应该是单独的层(如滑入式侧边菜单)没有显示出来,则可以使用CSS中的will-change属性向浏览器提示。

图13:主线程遍历布局树生成层树

主线程的栅格和合成

一旦创建了图层树并确定了绘制顺序,主线程便将该信息提交给合成线程。然后,合成器线程将每个图层栅格化。一层可能像页面的整个长度一样大,因此合成器线程将它们划分为图块,并将每个图块发送给栅格线程。栅格线程栅格化每个图块并将其存储在GPU内存中。

图14:光栅线程创建图块的位图并发送到GPU

合成器线程可以优先处理不同的栅格线程,以便可以首先对视口(或附近)中的事物进行栅格化。图层还具有用于不同分辨率的多个拼贴,以处理诸如放大动作之类的事情。

栅格化后,合成器线程会收集称为 draw quads 的图块信息以创建 compositor frame。

•Draw quads 包含诸如图块在内存中的位置以及在考虑页面合成的情况下在图块中绘制图块的位置之类的信息。

•Compositor frame 代表页面的框架的一个Draw quads集合

然后,合成器框架通过IPC提交给浏览器进程。此时,可以从UI线程(用于更改浏览器UI)或从其他渲染器进程(用于扩展)添加另一个合成器框架。这些合成器帧被发送到GPU,以将其显示在屏幕上。如果发生滚动事件,则合成器线程将创建另一个合成器框架以发送到GPU。

图15:合成器线程创建合成框架。将帧发送到浏览器进程,然后发送到GPU

合成的好处是它无需涉及主线程即可完成。合成器线程无需等待样式计算或JavaScript执行。这就是为什么仅合成动画被认为是最佳的平滑表现的原因。如果需要重新计算布局或绘画,则必须涉及主线程。

总结

在本文中,我们研究了从解析到合成的渲染管道。(注:后续的文章,第四篇更理论化了,就不再翻译了)

References

[1] Inside look at modern web browser (part 3):

[2] Web基础知识的性能部分:

[3] An introduction to error handling and strange cases in the parser:

[4] overview of the parsing model in the HTML spec:

[5] 文章:

[6] JavaScript模块:

[7] Resource Prioritization – Getting the Browser to Help You.:

[8] few talks from BlinkOn Conference:

[9] 演示视频:

[10] 优化JavaScript执行:

[11] 在Web Workers中运行JavaScript:

[12] 演示视频:

[13] 演示动画:

[14] 图层面板:

标签: #cssrepaints