[译]从内部了解现代浏览器(3)

3,811 阅读12分钟

原文Mariko Kosaka

[译]从内部了解现代浏览器(1)

[译]从内部了解现代浏览器(2)

[译]从内部了解现代浏览器(3)

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

这是本系列文章的的第3部分。 在前2篇,我们介绍了多进程架构和导航流程。在这篇文章中,我们将看看渲染器进程内部发生了什么。 渲染器进程涉及Web性能的许多方面。 由于渲染器过程中发生了很多事情,因此本文仅作为一般概述。 如果您想深入挖掘里面的细节,the Performance section of Web Fundamentals中有更多资源。

渲染器进程处理web内容

渲染器进程负责选项卡内的所有事情。 在渲染器进程中,主线程处理您发送给用户的大部分代码。 如果您使用web workerservice worker,JavaScript的一部分可能将由工作线程处理。合成器线程和光栅线程也在渲染器进程内运行,以高效,流畅地呈现页面。

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

图1:具有主线程,工作线程,合成器线程和栅格线程的渲染器进程

解析

构建dom

当渲染器进程收到导航的提交消息并开始接收HTML数据时,主线程开始解析文本字符串(HTML)并将其转换为文档对象模型(DOM); DOM是浏览器页面的内部表示,是Web开发人员可以通过JavaScript与之交互的数据结构和API。 HTML标准规范将HTML文档解析为DOM。 您可能已经注意到,即使是错误的HTML也不会抛出异常。 例如,缺少结束</p>标记。 像Hi! <b>I'm<i> Chrome </b>!</i>(b标签在i标签之前关闭)这样的错误标记会被视为<b>I'm<i> Chrome </i> </b> <i>!</i>。 这是因为HTML规范旨在优雅地处理这些错误。 如果您对如何完成这些工作感到好奇,可以阅读HTML规范中的“解析器中的错误处理和异常情况介绍”部分。

子资源加载

网站通常会使用图像,CSS和JavaScript等外部资源。 这些文件需要从网络或缓存中加载。 主线程可以在解析构建DOM时逐个请求它们,但为了加快速度,“预加载扫描器”会同时运行。 如果HTML文档中存在或之类的元素 ,则预加载扫描程序会检查由HTML解析器生成的标记,并向网络线程发送请求。

图2:主线程解析dom并生成dom树

JavaScript 可以阻止解析

当HTML解析器找到<script>标记时,它会停止解析HTML文档,并且必须加载,解析和执行JavaScript代码。 为什么? 因为JavaScript可以使用像document.write()那样改变整个DOM结构的东西来改变文档的结构(HTML规范中的模型解析概述有一个很好的图示)。 这就是HTML解析器在重新解析HTML文档之前必须等待JavaScript运行结束的原因。 如果您对JavaScript执行过程中发生的事情感到好奇,V8团队有对此的讨论和博客文章

提示浏览器如何加载资源

Web开发人员可以通过多种方式提示浏览器如何更好地加载资源。 如果您的JavaScript不使用document.write(),则可以向<script>标记添加asyncdefer属性。 然后,浏览器将异步加载和运行JavaScript代码,不会阻止DOM解析。 如果合适,您也可以使用JavaScript模块<link rel =“preload”>是一种通知浏览器当前导航必定需要该资源的方法,如果您希望尽快下载。 您可以在资源优先级 - 浏览器帮助您了解更多信息。

计算样式

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

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

即使您不提供任何CSS,每个DOM节点都具有它的计算样式。 <h1>标签字体大于<h2>标签,每个元素也都定义了边距。 这是因为浏览器具有默认的样式表。 如果您想知道Chrome的默认CSS是什么样的,您可以在此处查看源代码

布局

现在,渲染器进程知道每个节点的文档和样式的结构,但这不足以呈现页面。 想象一下,你正试图通过手机向朋友描述一幅画。 “有一个大的红色圆圈和一个小的蓝色方块”,这些并不足以让你的朋友了解这幅画的样子。

图4:一个人通过电话向别人描述一幅画的样子

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

图5:主线程通过计算样式遍历DOM树并生成布局树

确定页面的布局是一项具有挑战性的任务。 即使是最简单的页面布局,比如从上到下的标准流,也必须考虑字体的大小以及在哪里划分它们,因为它们会影响段落的大小和形状; 并且影响下一段的位置。

图6:由于换行符而移动的段落的盒子布局

CSS可以使元素浮动到一侧,隐去溢出项,并且更改写入的方向。 你可以想象,这个布局阶段是一项艰巨的任务。 在Chrome中,有一整个工程师团队负责布局。 如果你想看到他们工作的细节,很少有关于BlinkOn会议的演讲被记录下来,非常有趣。

绘制

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

图7:一个拿着画笔站在画布前面的人,想知道是应该先画圆圈还是先画方块

例如,可以为某些元素设置z-index,在这种情况下,按HTML中编写的元素顺序绘制将导致不正确的呈现。

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

在绘制步骤中,主线程遍历布局树以创建绘制记录。 绘画记录是一个绘画过程的注释,像是“背景优先,然后是文本,然后是矩形”。 如果您使用JavaScript绘制了<canvas>元素,那么您可能对此过程感到熟悉。

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

更新渲染流的成本很高

渲染流中最重要的是,在每个步骤中,都使用前面操作的结果来创建新数据 例如,如果布局树中的某些内容发生更改,则需要为文档的受影响部分重新生成“绘制”顺序

图10:DOM + Style,Layout和Paint树的生成顺序

如果要为元素设置动画,则浏览器必须在每个帧之间运行这些操作。 我们的大多数显示器每秒刷新屏幕60次(60 fps); 当你在每一帧移动屏幕时,动画对人眼来说是平滑的。 但是,如果动画遗漏了中间的帧,则页面就会出现“janky”。

图11:时间轴上的动画帧

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

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

您可以将JavaScript操作划分为小块,并计划使用requestAnimationFrame()在每一帧运行。 有关此主题的更多信息,请参阅优化JavaScript执行。 您也可以在Web Workers中运行JavaScript以避免阻塞主线程。

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

合成

你会如何绘制一个页面?

既然浏览器知道了文档的结构、每个元素的样式、页面的几何形状和绘制顺序,那么它如何绘制页面呢?将这些信息转换成屏幕上的像素称为光栅化。 处理这个问题的一种简单的方法可能是在视口内部使用光栅部件。如果用户滚动页面,则移动已光栅化的框架,并通过更多光栅填充缺少的部分。这是Chrome首次发布时处理光栅化的方式。然而,现代浏览器运行一个更复杂的过程,称为合成。

图14:简单光栅过程的动画

什么是合成

合成是一种将页面的各个部分分层,分别栅格化,并在称为合成器线程的单独线程中合成为页面的技术。 如果发生滚动,由于图层已经光栅化,因此它所要做的就是合成一个新帧。 通过移动图层和合成新帧,可以以相同的方式实现动画。 您可以使用“图层”面板查看您的网站在DevTools中如何划分为多个图层。

图15:合成过程的动画

分层

为了找出哪些元素需要在哪些层中,主线程遍历布局树以创建层树(此部分在DevTools性能面板中称为“更新层树”)。 如果页面的某些部分应该是单独的图层(如滑入式侧面菜单)却没有得到一个,那么您可以使用CSS中的will-change属性提示浏览器。

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

您可能想到给每个元素都添加层,但是在过多的层之间进行组合可能会导致操作速度比在每一帧中对页面的小部分进行光栅化要慢,因此度量应用程序的渲染性能至关重要。有关主题的更多信息,请参阅Stick to Compositor-Only Properties和Manage Layer Count.

光栅和合成器线程不涉及主线程

一旦创建了层树并确定了绘制顺序,主线程将该信息提交给合成器线程。然后合成线程将每个层进行栅格化。一个层可能很大,就像整个页面的长度一样,所以合成线程将它们分割成块,并将每个分块发送到光栅线程。光栅线程光栅化每个分块并将它们存储在GPU内存中。

图17:光栅线程创建位图并发送到GPU

合成器线程可以对不同的aster线程进行优先级排序,以便视口(或附近)内的事物可以先被光栅化。 一个层还具有多个不同分辨率的分块,可以处理放大操作等内容。

一旦分块被光栅化,合成器线程会收集平铺信息,称为绘制矩形,以创建一个合成帧

- -
绘制矩形 包含诸如分块在内存中的位置以及在考虑页面合成的情况下绘制分块的页面中的位置等信息。
合成帧 表示页面的帧的绘制矩形集合。

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

图18:合成器线程创建合成帧。先发送到浏览器进程,然后发送到GPU

合成的好处是它可以在不涉及主线程的情况下完成。 合成器线程不需要等待样式计算或JavaScript执行。 这就是为什么仅合成动画被认为是平滑性能的最佳选择。 如果需要再次计算布局或绘图,则必须涉及主线程。

总结

在这篇文章中,我们研究了从解析到合成的渲染过程。 希望您现在能够阅读更多关于网站性能优化的内容。 在本系列的下一篇也是最后一篇文章中,我们将更详细地研究合成器线程,看看当用户输入(如鼠标移动和单击)进入时发生了什么。 你喜欢这个帖子吗?如果您对以后的帖子有任何问题或建议,我很乐意在下面的评论部分或Twitter上@kosamari收到您的来信。