点击上方 前端Q,关注公众号
回复加群,加入前端Q技术交流群
提到浏览器不得不说Chrome,Chrome是Google发行的商业产品,而Chromium是一个开源版本的Chrome,两者很像但是不完全一样。
这里尝试将自己的理解结合下方PPT用最直白的语言记录最近了解到的浏览器的渲染原理知识,方便后续查阅。因为涉及到的知识点非常多且繁杂,如果有表述不到位的地方敬请谅解,错别字/错误理解之类的欢迎联系我修改。
为什么要做这件事?近几年浏览器更新挺大的,Chrome/Chromium整体还在不断演进,越来越频繁。本文有一些自己的理解如果有误欢迎联系
PPT内容整体来自Chromium开发者,Steve Kobes的演讲,PPT地址
长按识别二维码查看原文
https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit#slide=id.ga884fe665f_64_1691浏览器分层架构
简单的说浏览器作为应用,底层分别有content,Blink,V8,Skia等等,一层一层像套娃一样一层引用一层。对比普通应用的项目来说就是不断用第三方库和组件来拼凑应用,Chrome也不例外
content可以理解为就是除了浏览器主进程下的书签导航之外,网页内容这一部分,会随着网页不同而变化的部分
Blink渲染引擎,应该都听过就是网页的排版引擎,现存的Chrome/Edge都在用,作为开源项目维护,是在渲染进程里
Blink又嵌套了V8 JavaScript engine来执行JS代码
![c58af3dbec790ff585ac029b0c3c3197.png](https://i-blog.csdnimg.cn/blog_migrate/8657fd05d65c0fdc97b04e49c85f8788.png)
![92abeb8374e8ca6155a7c934b64b2ab3.png](https://i-blog.csdnimg.cn/blog_migrate/97d97b092da19e92797bbcf1b0b0bf69.png)
渲染
网页的渲染可以表示为Content经过rendering最后呈现的过程,即Code -> 可交互的页面
content是什么?
![54ba88ff8a93a4d36350060382ea031e.png](https://i-blog.csdnimg.cn/blog_migrate/ad2c67d209af4d57283bd55a929704cd.png)
可以看到content就是WebContents对象,C++代码的一个类。其代表的区域其实是标签页页打开的部分(上图红色部分)。而浏览器主进程还包含有地址栏、导航按钮、菜单、扩展,安全提示的小弹窗等等。
渲染进程render process是一个沙盒,基于安全考虑单独渲染进程render process挂了不会引起整个浏览器挂
渲染进程render process包含Blink渲染排版引擎和Chromium compositor(上图中绿色的CC简写)
![20950cb9f35a4132ff339a0c017d70e0.png](https://i-blog.csdnimg.cn/blog_migrate/79df20b2d9a21d787a13a0bcde497363.png)
![5f7e622a571ed0bcf5d445e3ae0f73e3.png](https://i-blog.csdnimg.cn/blog_migrate/2eab0953c63e049b347392d2f3bd3cd7.png)
content还表示网页内容代码,有HTML,CSS,JS,图片等,还有video,canvas,WebAssembly,WebGL等都可能在content区域显示或者运行
综上,content就是网页代码最后运行的结果,浏览器开发者工具可以看到最后是一个经过处理后的HTML的结构。「而这个HTML在渲染流水线里是一个输入」
像素是如何呈现的?
![24e1bb65e73236602940ef0e20656d1b.png](https://i-blog.csdnimg.cn/blog_migrate/4dc2f39e367415b68b097084efe00b57.png)
写过C/C++代码的同学知道,我们必须使用操作系统提供的底层API去画图,操作系统底层又去调用驱动程序,驱动程序驱动硬件。
今天大多数平台上都提供了“OpenGL”的标准化API。在Windows上有一个额外的DirectX转换。这些库提供诸如“纹理”和“着色器”之类的低级图形基元,并允许执行类似“在这些坐标处绘制一个三角形到虚拟像素缓冲区”之类的底层操作。未来计划用Vulkan替代Skia来做底层图形化调用。
所以渲染流水线的整个过程就是将输入的HTML、CSS、JS转化为OpenGL调用,最后在屏幕上呈现像素
浏览器渲染的目标
![217310eeb85d4269fc77fa7b3e490c96.png](https://i-blog.csdnimg.cn/blog_migrate/69467c8c6cb1df6a06f3642c8d8f0111.png)
初次渲染,将网页内容转化为底层OpenGL调用去显示页面
更新渲染,在JS运行,用户输入,异步请求或者滑动等交互介入后,再次渲染页面起到交互的目的,而且这里再次渲染需要高效执行,你会想到缓存对吗?是的每个阶段的结果为了提高渲染效率而被缓存下来。还有JS API会查询一些渲染数据如某个DOM节点的信息
拆分渲染阶段
![2f8a877ec0c4b8a71446bd3ee3f5e6ab.png](https://i-blog.csdnimg.cn/blog_migrate/7982257be601eb5221eeb4e3d803a59b.png)
把渲染管道分成多个阶段的话,可以看出来原来的content内容会被各个阶段stage处理为中间数据,最后才呈现为画面呈现出来。
![3b2f8d39b2d0c2a4f4cd7be53a16eed7.png](https://i-blog.csdnimg.cn/blog_migrate/4a18197c4a940d31ec22a5d327157b5c.png)
DOM
解析为DOM
![231dcb59706165caa0ad3b1584c650ff.png](https://i-blog.csdnimg.cn/blog_migrate/df00dd14628d470de1059909c06b1152.png)
![e6abde727d93a85e800bb5f544779a1b.png](https://i-blog.csdnimg.cn/blog_migrate/7208ed74b991c569c871e779babea22b.png)
HTML嵌套解析,解析时候解析为数据对象映射反应这里的嵌套模型
DOM(Document Object Model)是一棵树,树有父子,邻居的关系,而且这棵树是暴露API给JS调用,JS可以查询和修改这棵树。JS引擎V8通过bindings
的系统将DOM包装为DOM API供给Web开发者调用
文档可能包含多个DOM树
![fb7752cf1465336505de1413b0e2d6e3.png](https://i-blog.csdnimg.cn/blog_migrate/1fb03e738ee1b7b88e2d0744d6d5c1d5.png)
![88df97b844d5c6ed06b9d57710b24084.png](https://i-blog.csdnimg.cn/blog_migrate/df61fe7906b1f37122e20bdc3f7ddd96.png)
如上图的示例,自定义元素custom element有shadow tree。ShadowRoot的子元素其实被嵌入到slot元素里了,这跟各大前端框架的slot其实很像。
其实最后是在遍历树后合成视图,也就是两棵树合并为一棵树
Style
![7762712e3cb563b2d91b02bd7a55b67c.png](https://i-blog.csdnimg.cn/blog_migrate/fe03e0af217e3f1631b7d8da689d3b22.png)
![ec6d34455343adf1795052985de031f3.png](https://i-blog.csdnimg.cn/blog_migrate/ba463c0b1b35a2f9dcecbe7f92505fdf.png)
style步骤依赖前置的DOM树解析结果,选择器是选择DOM节点集合决定最后应用范围,最后样式生效是多个选择器共同作用的结果,而且样式间可能互相冲突导致没有按照预期运行,关于选择器的优先级感兴趣的同学自行查阅
![508e5211a8e1dfbd3ba6e857c3f30cda.png](https://i-blog.csdnimg.cn/blog_migrate/e93094ca71d0440b28b6163dc5aa5d54.png)
CSS解析器样式表StyleSheet构建样式规则。样式表可能位于<style>
元素、单独加载的资源的css文件中,也可能由浏览器默认提供。样式规则以各种方式编制索引以实现高效查找。
属性类在构建时由Python脚本自动生成,以声明方式定义了所有样式属性,如上图右上侧css_properties.json
经过py脚本转化为.cc
文件。
ComputedStyle
![53eafe1a614d7842015755b4057ded93.png](https://i-blog.csdnimg.cn/blog_migrate/b3eee17cb84d8b90cfae1264f9d35f87.png)
![1c5aa1a341ba31949099e5ad416e3dca.png](https://i-blog.csdnimg.cn/blog_migrate/b000831e9225ba68e8ecdefffdf22e37.png)
样式的重新计算(recalc)从活动样式表中获取所有解析的样式规则,并计算每个DOM元素的每个样式属性的最终值。这些内容存储在一个名为ComputedStyle的对象中,该对象只是样式属性到值的映射。可以看到其实每一个DOM节点都对应有一个ComputedStyle对象
在Chrome浏览器里的话,就是对应开发者工具的Computed样式属性这一栏。或者是通过getComputedStyle
的JSAPI去获取。
Layout
![68647e35acb888873e6b9416ab75e207.png](https://i-blog.csdnimg.cn/blog_migrate/2f93b9d5ce17004ae22ff29132964225.png)
在DOM和Style计算好后开始进入布局Layout阶段,比如将DIV解析为一个块级的LayoutReat区域,用x+y+width+height
来表示,布局就是为了计算x
,y
,width
,height
这些数据
![f6b5b19ca92389f58546b12aabbc74d1.png](https://i-blog.csdnimg.cn/blog_migrate/653bfb86d090692a27f5d64133553fcc.png)
默认情况下文档按照顺序排列下去形成了文档流
![fd1414300fa8dc86df76dc2cfa61a7a4.png](https://i-blog.csdnimg.cn/blog_migrate/20906fa7eb59d169b387176a7ba81fd8.png)
文字和内联元素则是左右浮动的,而且内联元素会被行尾打断(自动换行)。当然也有从右到左的语言,比如阿拉伯语和希伯来语
![044357d74a040727118f9d8ff7dafef6.png](https://i-blog.csdnimg.cn/blog_migrate/b00e2c8fdc84cd297e3354d9a7e21416.png)
布局也包括字体的排列,因为布局需要考虑文本在那里进行换行,Layout使用名为HarfBuzz的开源文本库来计算每个字形的大小和位置,这决定了文本的总宽度。字体成型必须考虑到排版特征,如字距调整letter-spacing
和连字。
![422c04b7de45fd3a7b7c44fedc5d34f5.png](https://i-blog.csdnimg.cn/blog_migrate/3debfb552b4665490b1bdb2158bf669f.png)
布局可以计算单个元素的多种边界矩形。例如,当存在溢出时,Layout将同时计算边界框和布局溢出。如果节点的溢出是可滚动的,Layout还会计算滚动边界并为滚动条预留空间。最常见的可滚动DOM节点是文档本身
![875600239c1c905bd3ffa7741f263e8f.png](https://i-blog.csdnimg.cn/blog_migrate/b97d74a4e5c730cb6e500977b2c8ab0b.png)
表格元素或display:table
的样式需要更复杂的布局,这些元素或样式指定将内容分成多列,或浮动对象漂浮在一边,内容在其周围流动,或者东亚语言的文本垂直排列,而不是水平排列。请注意DOM结构和ComputedStyle
值(如“Float:Left”)是布局算法的输入。「渲染流水线的每个阶段都会使用到前面阶段的结果」
![073edeff6c87e963c42dc3cbe8e2e8b1.png](https://i-blog.csdnimg.cn/blog_migrate/90dd5d8dcd9e517a73761b02278088f7.png)
通过遍历DOM树创建渲染树LayoutTree
,节点一一对应。布局树中的节点实现布局算法。根据所需的布局行为,LayoutObject
有不同的子类。比如LayoutBlockFlow就是块级Flow的文档节点。样式更新阶段也构建布局树。
在样式解析最后结束时需要构建布局树LayoutTree
,布局阶段遍历布局树,对布局树每个节点LayoutObject
执行布局,计算几何数据、换行符,滚动条等。
DOM节点跟Layout节点不一定是一一对应
![5b18194000735a9c9d107fd1839482ee.png](https://i-blog.csdnimg.cn/blog_migrate/ce267b56fc8024221f9c79ad0d965982.png)
一般情况下一个DOM节点会有一个LayoutObject,但是有时候LayoutObject是没有DOM节点与之对应的。
比如上图,span
标签外部没有section
标签嵌套,但是LayoutTree
会自动创建LayoutBlock
的匿名节点与之对应,再比如样式有display:none
的样式,那么也不会创建对应的LayoutTree
。
最后,如果是shadowTree的话,其LayoutObject节点可能会在不同的TreeScope
里
布局引擎正在重写
![98f627cb299c9f7f0419dbe2964239ba.png](https://i-blog.csdnimg.cn/blog_migrate/3d2292c640cab5d8353ebc77ae02ea20.png)
如上图所示,「LayoutNG」代表下一代的布局引擎,2020年布局引擎还在过渡阶段,所以有中间形态,如上图包含了LayoutObject
和LayoutNGMixin
混合节点。未来所有节点都会变成LayoutNG
的layout object
![fcb915e29381043178b91eaa59c41df0.png](https://i-blog.csdnimg.cn/blog_migrate/c6b62245cc2204762e70f0ca62bf1a2b.png)
NG节点的更新主要是因为之前的节点包含了输入、输出还有布局算法的信息,也就是说单个节点可以看到整棵树的状态(节点有可能需要获取父节点的宽高数据,但是父节点正在递归子节点布局中,实际上还没确定最后的布局)。
![bb2e894294278d6253b515fa1733d3f3.png](https://i-blog.csdnimg.cn/blog_migrate/baae68d173db7577ce6966c63a8f7e62.png)
而新的NG节点对输入和输出做了明显的区分,而且输出是immutable
的,可以缓存结果
![47b455b7e1f7dc69e1dec6cbc1b74129.png](https://i-blog.csdnimg.cn/blog_migrate/cffc7f4405c8810678006845e507791f.png)
布局结果指向描述物理几何的片段树,如图一个NGLayoutResult
对应几个NGPhysicalFragment
,对应右上角的几个矩形图形,如果NGLayoutResult
没变化则对应整块都不会变化。
举一个布局例子
![4bf8bd2f47668b223ce60d6d209100b4.png](https://i-blog.csdnimg.cn/blog_migrate/254053ca6edfd80118261fe4d3af4f59.png)
上图的HTML代码,会渲染如右下角的例子,对应的DOM树如左侧所示
![f166cd4b48621260499b3cf5928e51bd.png](https://i-blog.csdnimg.cn/blog_migrate/0d8e04ba2dce6326f2633436ccb02190.png)
DOM树跟Layout树很像,节点几乎是一一对应的,但是注意这里anonymous匿名节点被创建出来,它只有一个块级子元素。一个布局节点只能拥有块级元素或者内联元素其中之一
上图的子元素前面两个其实共享了匿名LayoutNGBlockFlow
,也就是说有共同的父节点
![e7aa4484644cb1029aa9772aa928d0e6.png](https://i-blog.csdnimg.cn/blog_migrate/c4705a204d41581ce4d855e37d1101bd.png)
![6d611780af647984f330397ddeecff47.png](https://i-blog.csdnimg.cn/blog_migrate/4024d96ec8d285b7f6963e5ce48ec895.png)
在「fragment tree」里我们可以更好的看到文本换行后的绘制结果,以及每个fragment的位置和大小
Paint
paint阶段只是创建绘制指令paint op
,页面还没有东西,甚至直到GL调用之前页面都是没有呈现任何东西的状态
创建绘制指令列表
![0662e4ebcae18349f025282634608815.png](https://i-blog.csdnimg.cn/blog_migrate/2713b0f65ef430f075aa3540475c7042.png)
绘制paint阶段创建绘制指令列表paint ops list
绘制指令paint op
可以理解为在某些坐标用什么颜色画一个矩形类似的意思,
每个布局对象LayoutObejct
可以有多个显示项目,对应于其视觉外观的不同部分,如背景、前景、轮廓等
样式可以控制绘制顺序
![3fbfc119d65fdb73e3cd5982383b7c3d.png](https://i-blog.csdnimg.cn/blog_migrate/f0210347b4539d4c160c75799550b595.png)
正确的绘制顺序非常重要,这样当元素重叠时,它们才能正确堆叠。顺序可以由样式控制,而不是完全依靠DOM的先后顺序
绘制分不同阶段进行
![5f615912ec60e174bb60a6f4f8d9008c.png](https://i-blog.csdnimg.cn/blog_migrate/904a8374322382c66d5a277dbf7af714.png)
每个绘制阶段「paint phase」都需单独遍历堆叠上下文staking context
。
一个元素甚至可能部分位于另一个元素的前面,部分位于另一个元素的后面。这是因为绘制在多个阶段中运行,每个绘制阶段都对自己的子树进行遍历。
举一个绘制例子
![9fc904365d3dc452c5f280faa7aefe1c.png](https://i-blog.csdnimg.cn/blog_migrate/f460e11978f5417470364fb2ec167986.png)
如上,一个样式和DOM节点渲染出来的结果,包含了四个绘制指令paint ops。
document
背景色绘制块级元素的背景色绘制
块级元素的前景色绘制(包含文本的绘制)
文本块的绘制
![8133b7eac21517397cef53937e4145f8.png](https://i-blog.csdnimg.cn/blog_migrate/dda280d6c7a844a8d5d26c1e62b5a446.png)
文本绘制操作包含文本块的绘制,其中包含每个字的字符和偏移量以及字体。如图这些数据都是HarfBuzz计算后得到的
raster
中文说的栅格化或者光栅化,本文取PS图层右键的栅格化为译文。熟悉PS的会知道矢量图形栅格化后放大图形会“糊”,但是不做栅格化处理直接放大矢量图形则不会。原因就是栅格化后只记录了单像素点的rgba值,放大后本来一个点数据要填满N个点,图像就”糊了“
raster将绘制指令转化为位图
![f010184e2e8ef35df07d09b5beb81f08.png](https://i-blog.csdnimg.cn/blog_migrate/1444c55a6fd9755065ee0bb58c242724.png)
把显示列表里的绘制操作执行的过程,成为任务,也称栅格化。比如PS里的合并图层任务,主要区别就是本来矢量的图任务后会变成位图bitmap,后面再缩放就会模糊。
生成的位图bitmap中的每个单元格都包含对单个像素的颜色和透明度进行编码的位。这里用十六进制FFFFFFFF表示一个点的rgba值
raster还可以对页面图片进行解码
![b9cf6fa26a502d05cb740105d4d4d57b.png](https://i-blog.csdnimg.cn/blog_migrate/8bb3204cd0c1f6cb34f517a5879bfa0f.png)
任务还可以对页面中嵌入的图像资源进行解码。绘制指令引用压缩数据(JPEG、PNG等),任务调用适当的解码器将其解压缩。
GPU加速栅格化
![97eb2492c92ae46dc1a0f6b64797d8e7.png](https://i-blog.csdnimg.cn/blog_migrate/3f5b1a0b445ee51460fd93f6573d0780.png)
GPU还可以运行生成位图的命令(“加速栅格化”)。请注意,这些像素还没有出现在屏幕上!
raster产生的位图数据存储在GPU内存中,通常是OpenGL纹理对象引用的GPU内存。
过去通常是存在内存里再传给GPU,但是现代GPU可以直接运行着色器shader并在GPU上生成像素,这种情况称为“加速栅格化”。但是两个结果都是一致的,最终内存(主存或者GPU内存)里拥有位图bitmap
raster通过Skia发出GL调用
GL调用即OpenGL调用,OpenGL意为"开放图形库",可以在不同操作系统、不同编程语言间适配2D,3D矢量图的渲染。
![ada218ba4532d8f43d339c67819c7e24.png](https://i-blog.csdnimg.cn/blog_migrate/3cc656d0be9e3b452fad1b36c31cd6ab.png)
raster通过名为Skia的库发出GL调用。Skia提供了围绕硬件的抽象层,如路径和贝塞尔曲线,子像素抗锯齿以及各种混合叠加模式。
Skia是开源的,由谷歌维护。跟随Chrome一起发布,但位于单独的代码库中。它也被其他产品使用,比如Android。Skia的GPU加速代码路径构建自己的绘制操作缓冲区,在栅格化结束时刷新。实际上发起GL调用的是Skia的后端,后面会说到
raster运行在GPU进程中
![a8ac2989fc7d0679cd63724da6333137.png](https://i-blog.csdnimg.cn/blog_migrate/7e5f1002fc9f4335195e62eddc928bad.png)
回想一下,渲染器进程是一个沙箱环境,因此它不能直接进行系统调用。绘制操作被运送到GPU进程进行任务处理。GPU进程可以发出实际的GL调用。除了独立于渲染器沙箱之外,在GPU进程中隔离图形化操作还可以保护我们免受不稳定或不安全的图形驱动程序的影响。比如GPU进程崩了,浏览器可以重启GPU进程
绘制请求通过命令缓冲区传递给GPU进程
![99f344909e286f3503526c290d29dd15.png](https://i-blog.csdnimg.cn/blog_migrate/8af799334a8275ece48302b9fa741518.png)
栅格化的绘制操作通过GPU命令缓冲区command buffer传输,渲染进程和GPU进程通过IPC通道发送。命令缓冲区command buffer最初是为序列化的GL图形命令构建的,类似一个proxy。当前的“进程外”栅格化(即GPU)以不同的方式使用它们,更多是绘制操作的包装器,就是命令缓冲区command buffer与底层图形API无关
不同操作系统调用不同的共享OpenGL库
![8b7d57ff5a7ca2568e27d173ee2ed436.png](https://i-blog.csdnimg.cn/blog_migrate/021226e8fcb03a2750d6f0074992c7ee.png)
GPU进程中的GL函数指针通过动态查找操作系统底层共享的OpenGL库进行初始化,Windows上用ANGLE做一个转化步骤。
Angel是另一个由Google构建的库;它的工作是将OpenGL转换为DirectX,DirectX是微软在Windows上用于加速图形的API。调查发现Angle比Windows的OpenGL驱动程序运行更好。
渲染流水线(简版)
![c0414b2a84df1bdb5b5c5495ccded3b1.png](https://i-blog.csdnimg.cn/blog_migrate/f93899412948442e71fc676b1a8d3df1.png)
至此拥有了整个pipeline,从DOM一直到内存中的像素,牢记渲染不是静态的,也不是执行一次就完成了,浏览器会话期间发生的任何事情都会动态更改渲染的过程。并且整个pipeline从头开始运行是非常昂贵的,尽可能希望能减少不必要的工作以提高效率
frames动画帧
![e30ea04aa0754325e3d8b7a29cf35631.png](https://i-blog.csdnimg.cn/blog_migrate/8ec7e4ae6ada889baa93f7ad2e708aed.png)
低于60帧每秒的动画和滚动看起来会非常卡,渲染器生成动画帧,每个帧都是内容在特定时间点状态的完整呈现,多个帧连起来就是看到的动画,其实动画只要达到60帧每秒那么看起来就会是连贯的。新的设备甚至要求90或120甚至更高的帧率。
如果在1/60秒内,约16.66ms还不能渲染完一帧画面,那么画面看起来就是断断续续很卡的样子
流水线各个阶段都依赖上一步的结果
![54dc6480805edcc32998c0756038b580.png](https://i-blog.csdnimg.cn/blog_migrate/966e6a40f5cd6a2bfc4c4496f97a2fe5.png)
为了提高性能,很简单的想到了尽可能复用上一阶段处理的结果,对于渲染来说既重复使用以前帧的输出
重绘
![cfb919786db082134da7be6a7107c3f0.png](https://i-blog.csdnimg.cn/blog_migrate/109251e30a95e985ed78975a96c05112.png)
大块区域的绘制和栅格化是非常昂贵的,比如在滚动的时候,视口内所有像素都变化了,这个过程称为重绘repaint
渲染进程主线程的竞争关系
![690e77b77878f7071d9c112143cf1937.png](https://i-blog.csdnimg.cn/blog_migrate/db5146f2dcb3a6b3b4b140c184f75487.png)
渲染进程的主线程的任何事情都会跟JS竞争(互斥关系),意味着其实JS也会阻塞渲染主线程其他任务的执行
分层与合成线程
![68baf578590f4359c5316607013d466a.png](https://i-blog.csdnimg.cn/blog_migrate/4cc8d1484ee181c62bcc385d8cf76cb9.png)
将页面分解为不同的层便于栅格化raster对不同的层单独处理,在渲染进程主线程构建层后commit
到合成线程compositor thread
去,合成线程compositor thread
会对每一层进行单独绘制
我们可以在浏览器开发工具的Layer看到当前页面的分层,分层的目的是可以对单独的层进行变换transform
和栅格化raster
试想一下如果有123三层,其中1,2两层没变化,第3层旋转了,那么只要对第三层每帧进行变换就可以得到每一帧的输出,计算量大大减少
「所以分层的目的是为了减少计算加速渲染效率,在渲染进程合成器线程执行则是为了不影响渲染主线程的任务执行」
图中的impl*即渲染进程的合成线程,因为历史原因在代码里都是这样表示,后面所有表示合成线程都用impl表示
为什么要分层?
![5aaeac52b6b9e577e070cad178077989.png](https://i-blog.csdnimg.cn/blog_migrate/ee8357ccc919ede9e7f5e73168f99545.png)
分层的作用在有动画时候可以显著提升性能,如图所示BBB文本一层的变换不会影响其他层
![f7b847f3d9f1963050d74c570c72a237.png](https://i-blog.csdnimg.cn/blog_migrate/bae38f2f9fd1b6586de029095f07d661.png)
动画是层的移动,页面滚动是层的移动和裁剪,放大缩小也是层的缩放
合成线程处理输入
![e969ce89c1890f5765f384b47d2e8f59.png](https://i-blog.csdnimg.cn/blog_migrate/9f87d62dd135366592636185d9a8adf4.png)
当滚动事件没有触发JS逻辑时候,即使渲染进程主线程很繁忙,但是浏览器进程发出的页面滚动事件的处理也不会受到影响,因为渲染进程的合成线程compositor thread
可以单独处理滚动事件
当然如果滚动触发了JS的逻辑,那么合成线程必须转发事件到主线程去,滚动事件会进入主线程任务队列等待处理
层的提升
![a73d428aca66c02549e6f9d9dacea4d7.png](https://i-blog.csdnimg.cn/blog_migrate/bf81b586865ecb3617e431c8a4108e04.png)
正常情况下一个LayoutView会创建一个PaintLayer,对应一个cc(Chromium Compositor) Layer。
但是某些样式属性也会导致对应的LayoutObject单独成层,比如transform属性就类似创建新层的“触发器”一样,浏览器遇到这个属性就会单独创建新层,cc(Chromium Compositor) Layer没有父子关系,是一个平级的列表,但是还是保留LayerTree的名称
滚动容器创建多个层
![1f800ca9548d9b0c33790bfd4648b0c3.png](https://i-blog.csdnimg.cn/blog_migrate/89824b423ee6fefc161487eae20172fd.png)
滚动容器创建特殊的多个层,比如元素加了overflow:scroll
的滚动属性,那么合成的时候会有5个层,其中4个层都是滚动条scrollbar的层,这些层合并起来称为CompositedLayerMapping
![3437eb16ff62bd50ca1a0c02d700173e.png](https://i-blog.csdnimg.cn/blog_migrate/b6f5b4fa3b327624f197d54d35ecbb12.png)
合成透明滚动条会禁用子像素抗锯齿,如上图左下角所示。而且判断是否合成滚动条也有判断逻辑,在安卓和ChromeOS上可以合成所有的滚动条
合成任务
![7ee4b4ced1918bdb17988e7fe557dc9b.png](https://i-blog.csdnimg.cn/blog_migrate/3c17a64b02eef1931e6129d576d20fb1.png)
如上图,合成任务包含构建层树的过程。在布局layout之后,绘制paint任务之前,这个过程也可以称为「分层和合成任务」,每一层layer都是独立绘制的,一些属性节点单独为层,比如will-change
,3D属性transform
之类
prepaint构建属性树
属性树
![38c5018dd41f9184ca55bf69917cc43b.png](https://i-blog.csdnimg.cn/blog_migrate/719f3db880d0f4f16bcd04e5321cae27.png)
渲染进程合成线程绘制的时候,合成线程里的合成器可以将各种属性应用于其绘制的图层,如变换矩阵,裁剪,滚动偏移,透明度。这些数据储存在属性树里,可以将这些理解为图层的属性(过去也是这么干的)。后面为了解耦这些属性,让它们可以脱离层单独使用,需要引入prepaint的过程
![b17047a71c69bec358f3f2841b7a72b1.png](https://i-blog.csdnimg.cn/blog_migrate/ad752327453271dfdecbf1e330cab4eb.png)
预绘制prepaint阶段遍历并构建属性树
合成后绘制(CAP)
CAP是composite after paint的缩写,它的目标是将属性和层解耦。即在paint阶段只需要paint的信息,而不需要知道层的任何信息,因为这时层还没有构建
![90a487a00625ce72672039cd2a83f2f6.png](https://i-blog.csdnimg.cn/blog_migrate/9921eb70eb965695ad58d227c8330f99.png)
在过去,变换、裁剪、效果滚动等信息等存储在层本身上,但CAP要求层的属性解耦。未来,层layer的合成会在绘制后进行
commit复制层数据到合成线程
![5d6ccd7ebcc1b8c58ee24b2135100410.png](https://i-blog.csdnimg.cn/blog_migrate/41a126cd375933d0dfd01ad6aa90d129.png)
在绘制paint阶段完成后,即绘制指令准备完成后,会进入渲染进程合成线程commit阶段
commit会拷贝层和属性树生成副本,这里合成线程的commit会阻塞主线程直到commit完成
「注意:渲染进程合成线程拿到的是layer副本,用LayerImpl
表示」
tiling分块平铺
![58e99519a3ef0039f47e5f987cb79fe8.png](https://i-blog.csdnimg.cn/blog_migrate/bdc655f202e98811fba49e2c77394727.png)
整个网页是非常大的,向下延伸理论上可以无限长(比如新闻类网站的无限滚动)。
栅格化是绘制之后的步骤,栅格化会将绘制指令转化为位图bitmap。试想一下如果在绘制完整个图层之后再栅格化整个图层,则成本会很大,但如果只栅格化部分图层的可见部分成本则会小很多。
这里tiling是平铺的意思,类似装修时候铺地板用大块瓷砖平铺,页面显示的做法类似。
根据视口viewport所在位置的不同,渲染进程合成器线程会选择靠近视口的图块tiles进行渲染,将最后选择渲染的图块传递给GPU栅格化线程池里的单个栅格化线程执行栅格化,最后得到栅格化好后的tile图块。图块大小根据不同设备的分辨率有不同的大小,比如256*256
或512*512
「图块tiles是栅格化任务的单位,栅格化就是将一块块的tiles转化为位图bitmap」
根据分块tiles去绘制层
![bd32be7c6dc9ebb95f6a23b92e011fc5.png](https://i-blog.csdnimg.cn/blog_migrate/d810c95aa41edf1bc7747f3bc42f8743.png)
在栅格化所有的图块tiles完成后,渲染进程的合成器线程会生成draw quads
命令。
quad类似于在屏幕上特定位置绘制图块tile的指令,draw quads
就是绘制图块们的意思。
此时的quad是层树layer tree
在拿属性树经过一堆变换后的最终结果,每个quad都引用图块tile在GPU内存里的栅格化输出结果。
多个DrawQuad
最后被包装在「CompositorFrame」里(简单理解就是一排要铺上去的瓷砖 :-),这是渲染进程最后的输出,包含有渲染进程生成的动画帧,会被传递给GPU进程。
「注意执行到这里还只是数据,这里屏幕还没有像素呈现」
activation
![d95ce74173c9c91bfaecf364cc211604.png](https://i-blog.csdnimg.cn/blog_migrate/25d0f600b58842570c94817ebf7d9670.png)
在准备图块tiles进行栅格化和draw两个阶段渲染进程的合成线程都会参与,但是渲染进程主线程里的layer数据还在不断commit过来。实际上合成线程具有两个树的拷贝副本
「pending tree」: 负责接收新的commit并转给栅格化线程池里的栅格化线程执行,完成后进入激活activation阶段,同步复制处理好后的layer副本到active tree里
「active tree」: 绘制上一次activation同步复制的layer副本(来自上一个commit)
这里pending tree
和 active tree
都是层列表和属性树的结合,不是真的树结构,基于习惯沿用树的叫法
display(Viz)
![fcb681ffa426e24d8b5ffca51b462dcb.png](https://i-blog.csdnimg.cn/blog_migrate/e4a1d5baede0d82663189ff6b8a9a309.png)
GPU进程的显示合成器display compositor
会将多个进程最后的「CompositorFrame」进行合并显示,前面说过「CompositorFrame」是每个进程最后的输出,包裹了DrawQuad
列表。
可以看到这里也有浏览器主进程的「CompositorFrame」,导航栏,收藏夹,前进后退这些Content外的渲染是浏览器主进程控制的。浏览器主进程有自己合成器为浏览器UI生成动画帧,比如标签条和地址栏的动画。
界面可以嵌入其他界面。浏览器嵌入渲染器,渲染器可以嵌入其他渲染器用于跨源iframe(也称为站点隔离,“进程外iframe”或OOPIF)。同源网页,比如iframe和一个标签页可能共用一个渲染进程,而跨源网页则一定是多个渲染进程。
长按识别二维码查看原文
https://www.chromium.org/developers/design-documents/oop-iframes显示合成器display compositor
在GPU进程中的Viz线程上运行。Viz取Visuals视觉效果的意思。
显示合成器display compositor
同步传入的帧,了解嵌入界面之间的依赖关系,做界面聚合。
Skia
![6db1ec61ebc02ca3971f3aef69529295.png](https://i-blog.csdnimg.cn/blog_migrate/44989b44f19d5fb6f8e046a7aab75159.png)
Viz线程除了做界面聚合还发起图形调用,最后屏幕上显示compositor frame
的quad。Viz线程是双缓冲的,分为前置缓冲区和后置缓冲区,这里将数据处理后序列化放到后置缓冲区
![cac790abad54803177b4fe24cca3fd59.png](https://i-blog.csdnimg.cn/blog_migrate/b9505090d393090535b5d86355ac5b80.png)
旧模式是GPU主线程解码器真正发起GL调用,新模式中是交给Skia库。Skia绘制到一个异步显示列表里,会一起传递到GPU主线程。GPU主线程的Skia后端发起真正的GL调用。
分离GL调用通过第三方的Skia或者未来准备使用的Vulkan实现与OpenGL解耦
前后缓冲区
在大多数平台上,显示合成器display compositor
的输出是双缓冲的,即包含前后两个缓冲区。图块绘制到后台缓冲区,Viz发出命令交换前后缓冲区使其可见
也就是说屏幕显示器这一帧的画面,是每HZ从前置缓冲区读取后在屏幕显示的,后置缓冲区在马不停歇的绘制,通过前后缓冲区的交换实现新一帧画面的呈现。在OS X上,使用CoreAnimation做了一些稍微不同的事情
显卡的作用?负责将数据写到后缓冲区,写完后前后缓冲区互换。通常情况下显卡的更新频率和显示器的刷新频率是一致的,如果不一致则会发现视觉上的卡顿。大多数设备屏幕的更新频率是60次/秒,这也就意味着正常情况下要实现流畅的动画效果,渲染引擎需要每秒更新60张图片到显卡的后缓冲区
至此浏览器完成了它的任务,底层驱动通过调用硬件完成绘制。最后,我们的像素出现在屏幕上
渲染流水线(全版)
![3bcc64c4deebf97e7baea2f56d151eca.png](https://i-blog.csdnimg.cn/blog_migrate/a6ad6e897cddf71a6edc5a1a326b8ca2.png)
回顾一下整个渲染流水线的过程,从渲染主线程获取Web内容,构建DOM树,解析样式,更新布局,layer分层后合成,生成属性树,创建绘制指令列表。
再到渲染进程合成线程收到渲染主线程commit过来的带有绘制指令和属性树的layer,将layer分块为图块,使用Skia对图块进行栅格化,拷贝pending tree
到active tree
,生成draw quads
命令,将quad发送给GPU的Viz线程,最后像素显示到屏幕上。
大多数阶段是在渲染器进程里执行的,但是raster和display则在GPU进程中执行。
核心渲染阶段DOM,style,layout,paint是在渲染进程主线程的Blink进行的,但是滚动和缩放等交互事件在渲染主线程繁忙时可以在渲染进程合成线程里执行
渲染进程主线程
「DOM:」 解析HTML生成DOM树
「style:」 解析
styleSheet
生成ComputedStyle
「layout:」 生成layout tree,跟DOM树基本对应,但是
display:none
的节点不显示,内联元素会创建LayoutBlock匿名节点包裹「layer分层后合成:」 某些样式属性会单独形成层,如
transform
会形成单独的层方便进行图形变换,滚动元素会多出scrollbar的4层。合成任务在渲染进程的合成线程中执行,与渲染主线程隔离互不影响「prepaint:」 为了将属性与层解耦引入prepaint阶段,prepaint阶段需要遍历并构建属性树,属性树即存储如变换矩阵,裁剪,滚动偏移,透明度等数据的地方,方便后面paint阶段拿属性树数据处理
「paint:」 绘制过程是将
LayoutObject
转化为绘制指令paint op
,每个LayoutObject
会对应多个绘制指令paint ops
,比如背景,前景,轮廓等。样式可以控制绘制的顺序。绘制有自己的顺序,如背景色在前,其次是浮动元素,前景色,轮廓outline
渲染进程合成线程
页面的滚动等交互会进入渲染进程合成线程compositor thread
里处理,这也是渲染进程主线程繁忙时交互也不卡的原因
「commit」: 渲染进程合成线程将层从渲染主线程拷贝出两份层和属性树副本
「tiling」: 栅格化整个图层成本大,渲染进程合成线程将layer分块后选择视口相近的图块tiles再进行栅格化成本小很多
「activate」: 合成线程具有两个树的副本,
pending tree
负责将新commit的layer转到栅格化线程池里的栅格化线程处理好后同步到active tree
「draw」: 栅格化所有变换后的图块之后,生成
draw quads
命令,包含多个DrawQuad的CompositorFrame
,这是渲染进程最后的输出,此时屏幕还没有像素出现
GPU进程
「raster」: 栅格化是将绘制指令
paint op
转化为位图bitmap的过程,转化后每个像素点的rgba都确定。栅格化还处理图片解码,通过调用不同解码器解压缩图片,GPU可以加速栅格化,通过调用Skia对图块进行栅格化「Skia」: 封装OpenGL调用,提供异步显示列表,最后传递到GPU主线程处理,GPU主线程的Skia后台发起真正的GL调用
「display」: GPU Viz线程里的显示合成器
display compositor
合并多个进程的「CompositorFrame」输出,并通过Skia发起图形调用,像素呈现在屏幕上
往期推荐
最后
欢迎加我微信,拉你进技术群,长期交流学习...
欢迎关注「前端Q」,认真学前端,做个专业的技术人...
点个在看支持我吧
参考
PPT: Life of a Pixel
长按识别二维码查看原文
https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit#slide=id.ga884fe665f_64_1691Youtube: Life of a Pixel
长按识别二维码查看原文
https://www.youtube.com/watch?v=K2QHdgAKP-s浏览器渲染机制(二)
长按识别二维码查看原文
https://juejin.cn/post/6920773802624286733