原文,Mariko Kosakajavascript
[译]从内部了解现代浏览器(2)html
[译]从内部了解现代浏览器(3)html5
这是本系列文章的的第3部分。 在前2篇,咱们介绍了多进程架构和导航流程。在这篇文章中,咱们将看看渲染器进程内部发生了什么。 渲染器进程涉及Web性能的许多方面。 因为渲染器过程当中发生了不少事情,所以本文仅做为通常概述。 若是您想深刻挖掘里面的细节,the Performance section of Web Fundamentals中有更多资源。java
渲染器进程负责选项卡内的全部事情。 在渲染器进程中,主线程处理您发送给用户的大部分代码。 若是您使用web worker
或service worker
,JavaScript的一部分可能将由工做线程处理。合成器线程和光栅线程也在渲染器进程内运行,以高效,流畅地呈现页面。web
渲染器进程的核心工做是将HTML,CSS和JavaScript转换为用户能够与之交互的网页。canvas
图1:具备主线程,工做线程,合成器线程和栅格线程的渲染器进程浏览器
当渲染器进程收到导航的提交消息并开始接收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树
当HTML解析器找到<script>
标记时,它会中止解析HTML文档,而且必须加载,解析和执行JavaScript代码。 为何? 由于JavaScript可使用像document.write()
那样改变整个DOM结构的东西来改变文档的结构(HTML规范中的模型解析概述有一个很好的图示)。 这就是HTML解析器在从新解析HTML文档以前必须等待JavaScript运行结束的缘由。 若是您对JavaScript执行过程当中发生的事情感到好奇,V8团队有对此的讨论和博客文章。
Web开发人员能够经过多种方式提示浏览器如何更好地加载资源。 若是您的JavaScript不使用document.write()
,则能够向<script>
标记添加async或defer属性。 而后,浏览器将异步加载和运行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树并生成布局树
肯定页面的布局是一项具备挑战性的任务。 即便是最简单的页面布局,好比从上到下的标准流,也必须考虑字体的大小以及在哪里划分它们,由于它们会影响段落的大小和形状; 而且影响下一段的位置。
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收到您的来信。