一、渲染流水线(全版)是在渲染流水线(简版)的基础上,增加如下内容:
frames动画帧
一、低于60帧每秒的动画和滚动看起来会非常卡,渲染器生成动画帧,每个帧都是内容在特定时间点状态的完整呈现,多个帧连起来就是看到的动画,其实动画只要达到60帧每秒那么看起来就会是连贯的。新的设备甚至要求90或120甚至更高的帧率。
二、如果在1/60秒内,约16.66ms还不能渲染完一帧画面,那么画面看起来就是断断续续很卡的样子
流水线各个阶段都依赖上一步的结果
一、为了提高性能,很简单的想到了尽可能复用上一阶段处理的结果,对于渲染来说既重复使用以前帧的中间环节的输出
重绘
一、大块区域的绘制和栅格化是非常昂贵的,比如在滚动的时候,视口内所有像素都变化了,这个过程称为重绘repaint
渲染进程主线程的竞争关系
一、渲染进程的主线程的任何事情都会跟JS竞争(互斥关系),意味着其实JS也会阻塞渲染主线程其他任务的执行
分层与合成线程
一、将页面分解为不同的层便于栅格化raster对不同的层单独处理,在渲染进程主线程构建层后commit到合成线程compositor thread去,合成线程compositor thread会对每一层进行单独绘制
【示例】我们可以在浏览器开发工具的Layer看到当前页面的分层,分层的目的是可以对单独的层进行变换transform和栅格化raster 试想一下如果有123三层,其中1,2两层没变化,第3层旋转了,那么只要对第三层每帧进行变换就可以得到每一帧的输出,计算量大大减少 |
---|
二、所以分层的目的是为了减少计算加速渲染效率,在渲染进程合成器线程执行则是为了不影响渲染主线程的任务执行
【示例】图中的impl*即渲染进程的合成线程,因为历史原因在代码里都是这样表示,后面所有表示合成线程都用impl表示 |
---|
三、可以通过Chrome Devtools -> more tools -> Layers 面板查看当前层
什么属性会引起Compositing Layers的形成
一、简单理解就是会动或者将来会动的部分
- transform: translateZ(0) 涉及到Z轴的变化,需要考虑层叠影响
- 普通的traslateX/traslateY不会,可以看DEMOopen in new window
- will-changeopen in new window 表示可能出现的变化,简单用GPU加速可以用这个属性而不要用transform: translateZ(0) ,因为transform: translateZ(0) 是个hack的写法,表示Z轴变化
- will-change也需要注意定位问题,可以看这里open in new window
- animation 设置了动画的元素,可以看这里DEMOopen in new window
- backface-visibility: hiddenopen in new window 指定当元素背面朝向观察者时是否可见 demoopen in new window
- 文档根元素,video、canvas、iframe 等元素
为什么要分层?
一、分层的作用在有动画时候可以显著提升性能,
【示例】如图所示BBB文本一层的变换不会影响其他层![]() ![]() |
---|
【示例】类似皮影戏,背景元素大体不懂,动的是任务和道具,单独一层方便控制,也减少消耗![]() |
---|
二、动画是层的移动,页面滚动是层的移动和裁剪,放大缩小是层的缩放
合成线程处理输入
一、当滚动事件没有触发JS逻辑时候,即使渲染进程主线程很繁忙,但是浏览器进程发出的页面滚动事件的处理也不会受到影响,因为渲染进程的合成线程compositor thread可以单独处理页面滚动事件
【示例】可以分别用Chrome/Safari打开这个DEMOopen in new window试一下,Chrome下JS阻塞了1S但是滚动不受影响,还是非常丝滑。而Safari你会感觉明显的卡顿 |
---|
二、如果滚动触发了JS的逻辑,那么合成线程必须转发事件到主线程去,滚动事件会进入主线程任务队列等待处理
层的提升
一、正常情况下一个LayoutTree会创建一个PaintLayer,PaintLayer是一个中间步骤(候选人),由它对应生成一个CompositorLayer合成层。
二、但是某些样式属性也会导致对应的LayoutObject单独成层,比如transform属性的Z轴变化就类似创建新层的“触发器”一样,浏览器遇到这个属性就会单独创建新层,CompositorLayer合成层没有父子关系,是一个平级的列表,但是还是保留LayerTree的名称
滚动容器创建多个层open in new window
一、滚动容器创建特殊的多个层,比如元素加了overflow:scroll的滚动属性,那么合成的时候会有5个层,其中4个层都是滚动条scrollbar的层,这些层合并起来称为CompositedLayerMapping
二、合成透明滚动条会禁用子像素抗锯齿,如上图左下角所示。而且判断是否合成滚动条也有判断逻辑,在安卓和ChromeOS上可以合成所有的滚动条
合成任务
一、如上图,compositing assignments合成任务包含构建层树的过程。在布局layout之后,绘制任务paint之前,这个过程也可以称为分层和合成任务,每一层layer都是独立绘制的,一些属性节点单独为层,比如will-change,3D属性transform之类
prepaint构建属性树
属性树
一、渲染进程合成线程绘制的时候,合成线程里的合成器可以将各种属性应用于其绘制的图层,如变换矩阵,裁剪,滚动偏移,透明度。这些数据储存在属性树里,可以将这些理解为图层的属性(过去也是这么干的)。后面为了解耦这些属性,让它们可以脱离层单独使用,需要引入prepaint的过程
二、预绘制prepaint阶段遍历并构建属性树
合成后绘制(CAP)
一、CAP是Composite After Paint的缩写,它的目标是将属性和层解耦。即在paint阶段只需要paint的信息,而不需要知道层的任何信息,因为这时层还没有构建
二、在过去,变换、裁剪、效果滚动等信息等存储在层本身上,但CAP要求层的属性解耦。未来,层layer的合成会在绘制后进行
三、CAP的进度可以看这里open in new window
commit复制层数据到合成线程
一、在绘制paint阶段完成后,即绘制指令准备完成后,会进入渲染进程合成线程commit阶段
二、commit会拷贝层和属性树生成副本,这里合成线程的commit会阻塞主线程直到commit完成
三、注意:渲染进程合成线程拿到的是layer副本,用LayerImpl表示
tiling分块平铺
一、整个网页是非常大的,向下延伸理论上可以无限长(比如新闻类网站的无限滚动)。
二、tiling是绘制paint之后,栅格化之前的步骤,栅格化会将绘制指令转化为位图bitmap。试想一下如果在绘制完整个图层之后再栅格化整个图层,则成本会很大,但如果只栅格化部分图层的可见部分成本则会小很多。
三、这里tiling是平铺的意思,类似装修时候铺地板用大块瓷砖平铺,页面显示的做法类似。
根据视口viewport所在位置的不同,渲染进程合成器线程会选择靠近视口的图块tiles进行渲染,将最后选择渲染的图块传递给GPU栅格化线程池里的单个栅格化线程执行栅格化,最后得到栅格化好后的tile图块。图块大小根据不同设备的分辨率有不同的大小,比如256256或512512
图块tiles是栅格化任务的单位,栅格化就是将一块块的512*512tiles转化为位图bitmap
根据分块tiles合成CompositorFrame
在栅格化所有的图块tiles完成后,渲染进程的合成器线程收集tiles的draw quads信息创建CompositorFrame。可以理解为一个房间的地板,有很多快小木地板拼起来
quad类似于在屏幕上特定位置绘制图块tile的指令,draw quads就是绘制图块们的意思。
此时的quad是层树layer tree在拿属性树经过一堆变换后的最终结果,每个quad都引用图块tile在GPU内存里的栅格化输出结果。
多个DrawQuad最后被包装在CompositorFrame里,这是渲染进程最后的输出,包含有渲染进程生成的动画帧,会被传递给GPU进程。
注意执行到这里还只是数据,这里屏幕还没有像素呈现
activation
渲染进程主线程里的layer数据还在不断commit过来的同时,渲染进程合成线程还要一边选择靠近视口的图块tiles送去GPU进行栅格化raster,还要接收栅格化raster后传递回来的数据进行draw。activation具体是指GPU进程回传的栅格化处理好的tile图块数据(下一帧)被激活的过程。实际上合成线程具有两个副本
- pending tree: 负责接收新的commit并转给栅格化线程池里的栅格化线程执行,完成后进入激活activation阶段,同步复制处理好后的layer副本到active tree里
- active tree: 绘制上一次activation同步复制的layer副本(来自上一个commit)
这里pending tree 和 active tree都是层列表和属性树的结合,不是真的树结构,基于习惯沿用树的叫法
display(Viz)
GPU进程的显示合成器display compositor会将多个进程最后的CompositorFrame进行合并显示,前面说过CompositorFrame是每个进程最后的输出,包裹了DrawQuad列表。
可以看到这里也有浏览器主进程的CompositorFrame,导航栏,收藏夹,前进后退这些Content外的渲染是浏览器主进程控制的。浏览器主进程有自己合成器为浏览器UI生成动画帧,比如标签条和地址栏的动画。
界面可以嵌入其他界面。浏览器嵌入渲染器,渲染器可以嵌入其他渲染器用于跨源iframe(也称为站点隔离,“进程外iframe”或OOPIFopen in new window)。同源网页,比如iframe和一个标签页可能共用一个渲染进程,而跨源网页则一定是多个渲染进程。
站点隔离可以参考这里文章的介绍open in new window
显示合成器display compositor在GPU进程中的Viz线程上运行。Viz取Visuals视觉效果的意思。
显示合成器display compositor同步传入的帧,处理嵌入界面之间的依赖关系,做界面合并。
Skia
GPU进程的Viz线程除了做界面聚合还发起图形调用,最后屏幕上显示compositor frame的quad。Viz线程是双缓冲的,分为前置缓冲区和后置缓冲区,这里将数据处理后序列化放到后置缓冲区。旧模式是GPU主线程解码器真正发起GL调用,新模式中是交给Skia库,这样隔离也方便Skia(OpenGL)升级Skia(Vulkan)
Skia绘制到一个异步显示列表里,会一起传递到GPU主线程。GPU主线程的Skia后端发起真正的GL调用。
分离GL调用通过第三方的Skia或者未来准备使用的Vulkan实现与OpenGL解耦
前后缓冲区
在大多数平台上,显示合成器display compositor的输出是双缓冲的,即包含前后两个缓冲区。图块绘制到后台缓冲区,Viz发出命令交换前后缓冲区使其可见
双缓冲机制open in new window
也就是说屏幕显示器这一帧的画面,是每HZ从前置缓冲区读取后在屏幕显示的,后置缓冲区在马不停蹄地绘制。当后置缓冲区绘制完毕后,通过前后缓冲区的交换实现新一帧画面的呈现。
显卡的作用?负责将数据写到后缓冲区,写完后前后缓冲区互换。通常情况下显卡的更新频率和显示器的刷新频率是一致的,如果不一致则会发现视觉上的卡顿。大多数设备屏幕的更新频率是60次/秒,这也就意味着正常情况下要实现流畅的动画效果,渲染引擎需要每秒更新60张图片到显卡的后缓冲区
至此浏览器完成了它的任务,底层驱动通过调用硬件完成绘制。最后,我们的像素出现在屏幕上