深入了解浏览器

969 阅读12分钟
最近在苦逼的帮女友查找其网站的一些性能问题(为什么程序员会有女朋友?),本来作为一名已经转了游戏的开发者来说可能有点显着狗逮耗子多管闲事的意思,但是本着对前端的热爱,和对技术孜孜不倦的追求(女友:你不帮我找我就哭给你看),还是闷下心去找了找可能出现的问题。 最终的结果先放在一边,在深入的了解了浏览器的renderer process之后,感觉像发现了新大陆,下面会着重的说一下浏览器的渲染器进程是如何工作的。这方面的东西都是理解性的东西,大部分都是得去看谷歌社区那些大佬们的讲解,文中很多地方都是有引用原文,当然,也有一大部分是自己的理解。

渲染器是何物

The renderer process is responsible for everything that happens inside of a tab. In a renderer process, the main thread handles most of the code you send to the user. Sometimes parts of your JavaScript is handled by worker threads if you use a web worker or a service worker. Compositor and raster threads are also run inside of a renderer processes to render a page efficiently and smoothly.

developers.google.com/web/updates…(翻墙)

渲染器进程负责选项卡(每个标签页)内发生的所有事情。 在渲染器进程中,主线程处理你为用户编写的大部分代码。 如果你使用了web worker或者 service worker ,有时JavaScript代码的一部分将由 Worker线程 处理。 合成器线程和栅格线程也在渲染器进程内运行,以便高效、流畅地呈现页面。

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

渲染器构成简介,主要是四个线程:

  • 主线程 Main thread
    •  负责解析html css 和主线程中的js,我们平时熟悉的那些东西,诸如:Calculate Style,Update Layer Tree,Layout,Paint,Composite Layers等等都是在这个线程中进行的。 总之,就是将我们的代码解析成各种数据,直到能被合成器线程接收去做处理。后面会对这一部分详细说明。
  • 合成器线程 Compositor thread
    • 处理用户输入的事件。(用户的任何手势,鼠标滚轮滚动是输入事件,触摸或鼠标悬停也是输入事件。
    • 将每个层拆分成tile(瓦片图)发送给栅格线程。

简单来说合成器线程的主要工作就是:从主线程中获取足够的信息,以响应将来的用户输入而独立地生成帧。

  • 栅格化线程 Raster thread
    • 可能不只一个线程,用于将tile图片栅格化并上载到GPU进程。
  • worker线程 Worker threads
    • 主要用于对dom无影响的计算。

主线程

提到主线程得先说一下浏览器的渲染流程

1.1渲染

浏览器渲染的主要流程为:

  • 处理HTML标记并构建DOM tree。
  • 处理CSS标记并构建CSSOM tree。
  • 将DOM和CSSOM合并到render tree中。
  • 在render tree上运行layout以计算每个节点的几何形状。
  • Paint:绘制。
  • Composite:合层。
  • 将各个节点绘制到屏幕上。

构建DOM(DOM tree)的流程:

  1. 转换:浏览器从磁盘或网络上读取HTML的原始字节,并根据文件的指定编码(例如UTF-8)将它们转换为单个字符。
  2. 标记化:浏览器将字符串转换为例如“ <html>”,“ <body>”以及尖括号内的其他字符串。每个标记都有特殊的含义和自己的规则集。
  3. 转换:发出的标记被转换为“对象”,以定义其属性和规则。
  4. DOM构建:最后,由于HTML标记定义了不同标记之间的关系(某些标记包含在其他标记中),因此创建的对象以树形数据结构链接,该树数据结构还捕获了原始标记中定义的父子关系:HTML对象是body对象的父对象,body是段落对象的父对象,依此类推。

整个过程的最终输出是我们页面的文档对象模型(DOM),浏览器将其用于页面的所有进一步的处理。此过程对应Chrome DevTools中的Parse Html。


DOM树捕获了文档标记的属性和关系,但是没有告诉浏览器元素在呈现时的外观。就好比现在你和朋友在玩一个‘我说你画’的游戏,你只是告诉了朋友“画一个圆圈,再画一个方块”,但是位置大小颜色这些都没告诉朋友,朋友听完一脸懵逼。


构建CSSOM(CSS Tree)的流程:

与HTML一样,现在将接收到的CSS规则转换为浏览器可以理解和使用的内容。
因此,我们重复HTML流程,但使用CSS而不是HTML:

CSSOM的构建步骤

(图片来源:链接)

CSS字节先转换为字符,然后转换为令牌,再转换为节点,最后将它们链接到称为“ CSS对象模型”(CSSOM)的树结构中:

CSSOM树

(图片来源:链接)

每种浏览器都提供一组默认样式,也称为“用户代理样式”(当我们不提供任何样式时,便会看到这些样式),而我们代码中的样式只会覆盖这些默认样式。如果你想知道Chrome的默认CSS是什么样的,点击查看

与DOM解析不同,DevTools不显示单独的“ Parse CSS”条目,而是捕获解析CSSOM树的构造,加上递归计算样式的时间。一般对应DevTools中的:


主线程基于HTML和CSS输入构建了DOM和CSSOM树。但是,这两个都是document不同方面的独立对象:一个描述内容,另一个描述需要应用于文档的样式规则。浏览器如何合并两者?

构建Render Tree的流程:

  1. 从DOM树的根开始,遍历每个可见节点。
    1. 一些节点不可见(例如,脚本标签,meta标签等),由于它们未反映在输出中,因此将其省略。
    2. 一些节点通过CSS隐藏,在渲染树中也被省略。注意visibility: hidden有所不同于display: none
  2. 对于每个可见节点,找到合适的CSSOM规则并应用它们。

  3. 输出每个可见节点具有的内容及其样式。

DOM和CSSOM结合在一起创建渲染树

(图片来源:链接)

最终产出一个Render Tree,其中包含屏幕上所有可见内容的内容和样式信息。

浏览器已经计算了哪些节点应该可见以及它们的样式,但是还没有计算它们在设备视口中确切位置和大小,这是layout阶段该做的事,也称为“重排”。 ”

在“我说你画“的游戏中,现在你能向朋友描述的信息是:”有一个大的红色圆圈和一个小小的蓝色方块“,但是这时朋友还是画不出你想要的东西。

人类传真机游戏

(图片来源:链接

Render Tree中储存节点渲染信息的对象叫做RenderObject(也叫LayoutObject)

layout的流程:

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

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

此步骤在DevTools中:


到此,你可以向朋友描述的信息更加丰富了:“有一个size这么大的红色圆圈在xy处,有一个size1这么大的蓝色方块在x1y1处”。此时可能你的朋友已经画出来你描述的图形了,但是还是有可能画错的,因为你的朋友还不知道以什么顺序绘制它们,即不知道谁应该覆盖谁。

绘画游戏

(图片来源:链接

paint的流程:

主线程遍历布局树以创建绘制记录。绘画记录是对绘画过程的注释,例如“首先是背景,然后是文本,然后是矩形”。

这里有一点需要解释一下,paint操作中并没有生成像素,它只是生成了 paint records,生成的
 paint records会被序列化放进浏览器的特定数据结构中方便后面的栅格化。主线程很忙,栅格化等像素操作应该由专门的“工作人员”去做。

The interest area is the region around the viewport for which SkPictures are recorded. When the DOM has changed, e.g. because the style of some elements are now different from the previous main-thread frame and have been invalidated, Blink paints the regions of invalidated layers within the interest area into an SkPicture-backed GraphicsContext. This doesn’t actually produce new pixels, rather it produces a display list of the Skia commands necessary to produce those new pixels. This display list will be used later to generate new pixels at the compositor’s discretion.

浏览器关注的区域是在视口内而且SKPictures中已经记录过的绘图信息当DOM发生更改时(例如,由于某些元素的样式现在与之前的主线程帧已不同并且已失效),浏览器将关注区域内无效层的区域绘制到SkPicture支持的GraphicsContext中。这实际上并不会产生新的像素,而是会产生产生这些新像素所需的Skia命令的显示列表。该显示列表将在以后根据合成器的调度用于生成新像素。

此步骤在DevTools中:


layout一定会触发paint。

Composite:合层

在过去,如果我们想进行 div 滚动,浏览器需要重绘出每一帧。这意味着如果用户一直拖动滚轮,我们就需要生成所有的像素点,用户需要等待我们运行整个渲染流水线后才可以继续滚动动。



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


合层相关的知识很多也很复杂,会涉及到如何去分层,而且前面几步中也会有一些数据来供合层和分层的时候使用,下面会详细说一下主线程如何去处理这些的:

上面提到生成Render Tree的过程中记录信息的对象叫做RenderObject,现在又有一个新的概念叫做RenderLayer:共享相同坐标空间(例如,受相同CSS变换影响)的RenderObject通常属于同一RenderLayer。每个RenderObject都直接或通过祖先RenderObject间接与RenderLayer关联。

为了方便理解,我们还是拿上面“我说你画”的游戏来举例,现在“我说你画”的游戏加了一个新规则,是要根据你的描述,让朋友的画作中某些部分做一些特殊的效果,比如让一个方块和其内部的所有东西透明度都变为原来的一半,比如把某个圆圈和其内部的东西一起放到左上角,这时你的朋友需要一个牛逼的技能,就是将这些受改变的元素及其子元素放到另一张透明的纸上(纸是透明的,但是元素该什么样还是什么样),然后当元素发生变换时,直接操作这张纸上的东西,然后在拍在原来的纸上,就大功告成了。这样的好处就是不用去一个一个挪所有东西或者一个一个的去调节元素的透明度,只需要操作这张透明纸就行了。

根据创建 RenderLayer 的原因不同,可以将其分为常见的 3 类:

NormalPaintLayer
  • 根元素(HTML)
  • 有明确的定位属性(relative、fixed、sticky、absolute)
  • 透明的(opacity 小于 1)
  • 有 CSS 滤镜(fliter)
  • 有 CSS mask 属性
  • 有 CSS mix-blend-mode 属性(不为 normal)
  • 有 CSS transform 属性(不为 none)
  • backface-visibility 属性为 hidden
  • 有 CSS reflection 属性
  • 有 CSS column-count 属性(不为 auto)或者 有 CSS column-width 属性(不为 auto)
  • 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画
OverflowClipPaintLayer
  • overflow 不为 visible
NoPaintLayer
  • 不需要 paint 的 PaintLayer,比如一个没有视觉属性(背景、颜色、阴影等)的空 div。

满足以上条件的 LayoutObject 会拥有独立的渲染层,而其他的 LayoutObject 则和其第一个拥有渲染层的父元素共用一个。

个人的一些理解:浏览器不只要应对纯静态的网页,也要应对其中复杂的页面效果,所以如何用更低的性能消耗来实现复杂的效果便是浏览器最需要解决的。比如上述生成Render Layer其实目的也是为了方便,包括后面要说的Graphics Layer也都是为了更加高效的去处理

为什么要合层,为什么要分层:

1. 上面说的现代浏览器与老旧浏览器处理画面的区别是诸如滚动时如何生成画面,那么无论什么情况都只生成一层行不行?当然是不行的,首要的就是浏览器尽量避免“牵一发而动全身”,也就是避免不必要的重绘重排。想象一下如果不分层,那么每当页面上一个元素发生变化,那么浏览器要去重绘甚至重排界面内所有的元素,多么恐怖的开销。

2. 利用硬件加速高效实现某些UI特性。例如网页的某一个Layer设置了可滚动、3D变换、透明度或者滤镜,那么就可以通过GPU来高效实现。

未完待续。。

合成的产出是主线程中最终要交给合成器线程去处理的数据。

后面会补充合层的原理,合成器线程工作流程,光栅线程工作流程以及和gpu如何协调配合。。。ps:本来想一周写完的东西,3周写了不到1/3,折服于我的语文水平以及英文水平。。。

参考链接

developers.google.com/web/fundame…

juejin.cn/post/684490…

www.youtube.com/watch?v=ExN…

juejin.cn/post/684490…

dev.chromium.org/developers/…

www.chromium.org/developers/…

fed.taobao.org/blog/taofed…

developers.google.com/web/fundame…

developers.google.com/web/fundame…

www.cnblogs.com/wgwyanfs/p/…

www.cnblogs.com/mfmdaoyou/p…

blog.csdn.net/luoshengyan…

developers.google.com/web/fundame…

developers.google.com/web/fundame…

developers.google.com/web/updates…

www.chromium.org/developers/…