现代浏览器探秘(part3):渲染

翻译:疯狂的技术宅 原文:developers.google.com/web/updates…javascript

现代浏览器探秘(part1):架构css

现代浏览器探秘(part2):导航html

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

这是关于浏览器内部工做原理系列的第3部分。 以前,咱们介绍了多进程架构导航流程。 在这篇文章中,咱们将看看渲染器进程内部发生了什么。java

渲染进程涉及Web性能的诸多方面。 因为渲染进程中发生了不少事情,所以本文不能一一赘述。 若是你想深刻挖掘,能够在Web基础的性能部分找到更多内容。web

渲染器进程处理Web内容

渲染器进程负责选项卡内发生的全部事情。 在渲染器进程中,主线程处理你为用户编写的大部分代码。 若是你使用了web worker 或 a service worker,有时JavaScript代码的一部分将由工做线程处理。 排版和栅格线程也在渲染器进程内运行,以便高效、流畅地呈现页面。chrome

渲染器进程的核心工做是将HTML、CSS和JavaScript转换为用户能够与之交互的网页。canvas

图1:渲染器进程内部有主线程、工做线程、排版线程和栅格线程

图1:渲染器进程内部有主线程、工做线程、排版线程和栅格线程浏览器

解析

构建DOM

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

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规范中的“解析器中的错误处理和奇怪状况介绍”部分。

子资源加载

网站一般使用图像、CSS和JavaScript等外部资源。 这些文件须要从网络或缓存中加载。 主线程能够在解析构建DOM时会逐个请求它们,但为了加快速度,“预加载扫描器”也会同时运行。 若是HTML文档中存在<img><link>之类的内容,则预加载扫描器会检查由HTML解析器生成的标记,并在浏览器进程中向网络线程发送请求。

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

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

JavaScript能够阻止解析

当HTML解析器找到<script>标记时,它会暂停解析HTML文档,而且必须加载、解析和执行JavaScript代码。 为何要这样处理? 由于JavaScript可使用像document.write() 那样改变整个DOM结构的东西来改变文档的形状(HTML规范中的解析模型概述有一个很好的示意图)。 这就是HTML解析器在从新解析HTML文档以前必须等待JavaScript运行的缘由。 若是你对JavaScript执行中发生的事情感到好奇,V8团队的博客对此进行了讨论。

提示浏览器如何加载资源

Web开发人员能够经过多种方式向浏览器发送提示,以便很好地加载资源。 若是你的JavaScript不使用 document.write(),则能够向<script>标记添加asyncdefer属性。 而后,浏览器异步加载和运行JavaScript代码,不会阻止解析。 若是合适,你也可使用JavaScript模块<link rel ="preload">是一种通知浏览器当前导航确定须要这个资源的方法,你但愿尽快下载。 你能够在资源优先级找到更多信息。

样式表计算

拥有DOM不足以知道页面的外观,由于咱们能够在CSS中设置页面元素的样式。 主线程解析CSS并肯定每一个DOM节点的计算样式。 这是有关基于CSS选择器将哪一种样式应用于每一个元素的信息。 你能够在浏览器中开发者工具中的computed部分中看到此信息。

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

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

即便你不提供任何CSS,每一个DOM节点都具备计算样式。好比 <h1>标签的显示要大于<h2>标签,同时为每一个元素定义边距。 这是由于浏览器具备默认样式表。 若是你想知道Chrome的默认CSS是什么样的,你能够在此处查看源代码

布局

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

图4:一我的站在一幅画,经过电话线与另外一我的联系

图4:一我的站在一幅画,经过电话线与另外一我的联系

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

图5:主线程经过DOM树生成计算样式和布局树

图5:主线程经过DOM树生成计算样式和布局树

肯定页面布局是一项具备挑战性的任务。 即便是最简单的页面布局,如从上到下的块流,也必须考虑字体的大小以及在哪里划分它们,由于它们会影响段落的大小和形状; 而后影响下一段所需的位置。

图6:因为换行符而移动的段落的框布局

CSS可使元素浮动到一侧,掩盖溢出项,并更改写入方向。 你能够想象,这个布局阶段是一项艰巨的任务。 在Chrome项目中,有一个完整的工程师团队负责布局。 若是你想看到他们工做的细节,看看这些会议记录很是有意思。

绘制

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

图7:一个在画布前拿着画笔的人,正在思考是应该先画圆圈仍是矩形

图7:一个在画布前拿着画笔的人,正在思考是应该先画圆圈仍是矩形

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

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

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

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

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

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

更新渲染通道的成本很高

在渲染通道中最重要的一件事就是在每一个步骤中,前一个操做的结果被用于建立新数据。 例如:若是布局树中的某些内容发生更改,则须要为文档的受影响部分从新生成绘制顺序。

图10:DOM + Style,布局和绘制树的生成顺序

若是要为元素设置动画,则浏览器必须在每一个帧之间运行这些操做。 咱们的大多数显示器每秒刷新屏幕60次(60 fps); 当你在每一帧移动屏幕时,动画对人眼来讲会很平滑。 可是若是动画错过了其中的帧,则页面将发生闪烁。

图11:时间轴上的动画帧

图11:时间轴上的动画帧

即便你的渲染操做可以跟上屏幕刷新,这些计算也是在主线程上运行的,这意味着当你的应用运行 JavaScript 时它可能会被阻止。

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

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

你能够将JavaScript操做划分为小块,并使用 requestAnimationFrame() 安排在每一个帧上运行。 有关此主题的更多信息,请参阅优化JavaScript执行。 你也能够在 Web Workers 中运行 JavaScript 来避免阻塞主线程

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

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

合成

你会如何绘制一个页面?

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

图14:简单光栅化过程

也许处理这种状况的一种简单的方法是在视口(viewport)内部使用栅格部件。 若是用户滚动页面,则移动光栅帧,并经过更多光栅填充缺乏的部分。 这就是Chrome首次发布时处理栅格化的方式。 可是,现代浏览器运行一个称为合成的更复杂的过程。

什么是合成

合成是一种将页面的各个部分分层,分别栅格化,并在一个被称为合成器线程的独立线程中合成为页面的技术。 若是发生滚动,因为图层已经被栅格化,因此它所要作的就是合成一个新帧。 经过移动图层和合成新帧,能够用相同的方式实现动画。

图15:合成过程的示意动画

你可使用浏览器开发者工具的“layout”面板中查看你的网站如何划分为多个图层

分为几层

为了找出哪些元素须要放在哪些层中,主线程经过遍历布局树以建立层树(此部分在DevTools性能面板中称为“Update Layer Tree”)。 若是页面某些应该是单独图层(如滑入式侧面菜单)的部分可是没有分配到图层,那么你可使用CSS中的will-change属性提示浏览器。

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

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

也许你想要为每一个元素提供图层,可是过多的图层进行合成可能会致使比每帧光栅化页面的小部分更慢的操做,所以测量应用程序的渲染性能相当重要。 有关主题的更多信息,请参阅Stick to Compositor-Only Properties and Manage Layer Count

光栅和复合关闭主线程

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

图17:栅格线程建立tile位图并发送到GPU

图17:栅格线程建立tile位图并发送到GPU

合成器线程能够优先考虑不一样的aster线程,以便视口(或附近)内的事物能够先被光栅化。 图层还具备多个不一样分辨率的倾斜度,能够处理放大操做等内容。

一旦tile被光栅化,合成器线程会收集称为绘制四边形(draw quads )的tile信息来建立合成器帧(compositor frame)

绘制四边形 包含信息,例如图块在内存中的位置以及在考虑页面合成的状况下绘制图块的页面中的位置。
合成器帧 表示页面帧的绘制四边形的集合。

而后经过IPC将合成器帧提交给浏览器进程。这时能够从UI线程添加另外一个合成器帧以用于浏览器UI更改,或者从其余渲染器进程添加扩充数据。 这些合成器帧被发送到GPU用来在屏幕上显示。 若是发生滚动事件,合成器线程会建立另外一个合成器帧并发送到GPU。

图18:合成器线程建立合成帧。 帧先被发送到浏览器进程,而后再发送到GPU

图18:合成器线程建立合成帧。 帧先被发送到浏览器进程,而后再发送到GPU

合成的好处是它能够在不涉及主线程的状况下完成。 合成线程不须要等待样式计算或 JavaScript 执行。 这就是合成动画是平滑性能的最佳选择的缘由。 若是须要再次计算布局或绘图,则必须涉及主线程。

总结

在本文中,咱们研究了从解析到合成的渲染通道。

在本系列的下一篇文章中,咱们将更详细地介绍合成器线程,并了解当用户进行鼠标移动和单击等操做时会发生什么。

原文首发于京程一灯公众号:jingchengyideng

相关文章
相关标签/搜索