像素的一生—浏览器渲染流水线简述

提到浏览器不得不说Chrome,Chrome是Google发行的商业产品,而Chromium是一个开源版本的Chrome,两者很像但是不完全一样。
这里尝试将自己的理解结合下方PPT用最直白的语言记录最近了解到的浏览器的渲染原理知识,方便后续查阅。因为涉及到的知识点非常多且繁杂,如果有表述不到位的地方敬请谅解,错别字/错误理解之类的欢迎联系我修改。

为什么要做这件事? 近几年浏览器更新挺大的,Chrome/Chromium整体还在不断演进,越来越频繁。本文有一些自己的理解如果有误欢迎联系

PPT内容整体来自Chromium开发者,Steve Kobes的演讲,PPT地址open in new window

浏览器分层架构

简单的说浏览器作为应用,底层分别有content,Blink,V8,Skia等等,一层一层像套娃一样一层引用一层。对比普通应用的项目来说就是不断用第三方库和组件来拼凑应用,Chrome也不例外

  • content可以理解为就是除了浏览器主进程下的书签导航之外,网页内容这一部分,会随着网页不同而变化的部分
  • Blink渲染引擎,应该都听过就是网页的排版引擎,现存的Chrome/Edge都在用,作为开源项目维护,是在渲染进程里
  • Blink又嵌套了V8 JavaScript engine来执行JS代码

浏览器多进程架构简单介绍

浏览器不是单进程结构,发展到现在已经是多进程结构了,且2018年的结论是Chromium还在不断拆离主进程里的公共服务作为独立service对外提供服务。可以参考微服务架构

浏览器多进程架构简单介绍可以参考这里文章的介绍open in new window

渲染

网页的渲染可以表示为Content经过rendering最后呈现的过程,即Code -> 可交互的页面

content是什么?

可以看到content就是WebContents对象,C++代码的一个类。其代表的区域其实是标签页页打开的部分(上图红色部分)。而浏览器主进程还包含有地址栏、导航按钮、菜单、扩展,安全提示的小弹窗等等。

渲染进程render process是一个沙盒,基于安全考虑单独渲染进程render process挂了不会引起整个浏览器挂

渲染进程render process包含Blink渲染排版引擎和Chromium compositor(上图中绿色的CC简写),这么分是因为渲染进程里有主线程和合成线程。

content还表示网页内容代码,有HTML,CSS,JS,图片等,还有video,canvas,WebAssembly,WebGL等都可能在content区域显示或者运行

综上,content就是网页代码最后运行的结果,浏览器开发者工具可以看到最后是一个经过处理后的HTML的结构。而这个HTML在渲染流水线里是一个输入

像素是如何呈现的?

写过C/C++代码的同学知道,我们必须使用操作系统提供的底层API去画图,操作系统底层又去调用驱动程序,驱动程序驱动硬件。

今天大多数平台上都提供了“OpenGL”的标准化API。在Windows上有一个额外的DirectX转换。这些库提供诸如“纹理”和“着色器”之类的低级图形基元,并允许执行类似“在这些坐标处绘制一个三角形到虚拟像素缓冲区”之类的底层操作。未来计划用Vulkan替代Skia来做底层图形化调用。

所以渲染流水线的整个过程就是将输入的HTML、CSS、JS转化为OpenGL调用,最后在屏幕上呈现像素

浏览器渲染的目标

  1. 初次渲染,将网页内容转化为底层OpenGL调用去显示页面
  2. 更新渲染,在JS运行,用户输入,异步请求或者滑动等交互介入后,再次渲染页面起到交互的目的,而且这里再次渲染需要高效执行,你会想到缓存对吗?是的每个阶段的结果为了提高渲染效率而被缓存下来。还有JS API会查询一些渲染数据如某个DOM节点的信息

拆分渲染阶段

把渲染管道分成多个阶段的话,可以看出来原来的content内容会被各个阶段stage处理为中间数据,最后才呈现为画面呈现出来。

DOM

解析为DOM

HTML嵌套解析,解析时候解析为数据对象映射反应这里的嵌套模型

DOM(Document Object Model)是一棵树,树有父子,邻居的关系,而且这棵树是暴露API给JS调用,JS可以查询和修改这棵树。JS引擎V8通过bindings的系统将DOM包装为DOM API供给Web开发者调用

文档可能包含多个DOM树

如上图的示例,自定义元素custom element有shadow tree。ShadowRoot的子元素其实被嵌入到slot元素里了,这跟各大前端框架的slot其实很像。

其实最后是在遍历树后合成视图,也就是两棵树合并为一棵树

Style

style步骤依赖前置的DOM树解析结果,选择器是选择DOM节点集合决定最后应用范围,最后样式生效是多个选择器共同作用的结果,而且样式间可能互相冲突导致没有按照预期运行,关于选择器的优先级感兴趣的同学自行查阅

CSS解析器样式表StyleSheet构建样式规则。样式表可能位于<style>元素、单独加载的资源的css文件中,也可能由浏览器默认提供。样式规则以各种方式编制索引以实现高效查找。
属性类在构建时由Python脚本自动生成,以声明方式定义了所有样式属性,如上图右上侧css_properties.jsonopen in new window经过py脚本转化为.cc文件。

ComputedStyle

样式的重新计算(recalc)从活动样式表中获取所有解析的样式规则,并计算每个DOM元素的每个样式属性的最终值。这些内容存储在一个名为ComputedStyle的对象中,该对象只是样式属性到值的映射。可以看到其实每一个DOM节点都对应有一个ComputedStyle对象

在Chrome浏览器里的话,就是对应开发者工具的Computed样式属性这一栏。或者是通过window.getComputedStyle的JSAPI去获取。

Layout

在DOM和Style计算好后开始进入布局Layout阶段,比如将DIV解析为一个块级的LayoutRect区域,用x+y+width+height来表示,布局就是为了计算x,y,width,height这些数据

默认情况下文档按照顺序排列下去形成了文档流

文字和内联元素则是左右浮动的,而且内联元素会被行尾打断(自动换行)。当然也有从右到左的语言,比如阿拉伯语和希伯来语

布局也包括字体的排列,因为布局需要考虑文本在那里进行换行,Layout使用名为HarfBuzz的开源文本库来计算每个字形的大小和位置,这决定了文本的总宽度。字体成型必须考虑到排版特征,如字距调整letter-spacing和连字。

布局可以计算单个元素的多种边界矩形。例如,当存在溢出时,Layout将同时计算边界框和布局溢出。如果节点的溢出是可滚动的,Layout还会计算滚动边界并为滚动条预留空间。最常见的可滚动DOM节点是文档本身

表格元素或display:table的样式需要更复杂的布局,这些元素或样式指定将内容分成多列,或浮动对象漂浮在一边,内容在其周围流动,或者东亚语言的文本垂直排列,而不是水平排列。请注意DOM结构和ComputedStyle值(如“Float:Left”)是布局算法的输入。渲染流水线的每个阶段都会使用到前面阶段的结果

通过遍历DOM树创建渲染树LayoutTree,节点一一对应。布局树中的节点实现布局算法。根据所需的布局行为,LayoutObject有不同的子类。比如LayoutBlockFlow就是块级Flow的文档节点。样式更新阶段也构建布局树。

在样式解析最后结束时需要构建布局树LayoutTree,布局阶段遍历布局树,对布局树每个节点LayoutObject执行布局,计算几何数据、换行符,滚动条等。

DOM跟Layout的节点不一定是一一对应

一般情况下一个DOM节点会有一个LayoutObject,但是有时候LayoutObject是没有DOM节点与之对应的。
比如上图,span标签外部没有div标签嵌套,但是LayoutTree会自动创建LayoutBlock的匿名节点与之对应,再比如样式有display:none的样式,那么也不会创建对应的LayoutTree

伪类的作用也是在LayoutTree上的,如p::before{content:"Hi!"}也不在DOM里而是在Layout Tree上。可以理解DOM是本体,伪类是给本体的“化妆”,比如伪娘本体是男的,但是看着像女的

最后,如果是shadowTree的话,其LayoutObject节点可能会在不同的TreeScope里。

布局引擎正在重写

如上图所示,LayoutNG代表下一代的布局引擎,2020年布局引擎还在过渡阶段,所以有中间形态,如上图包含了LayoutObjectLayoutNGMixin混合节点。未来所有节点都会变成LayoutNGlayout object

RenderingNG: 包含大量重构和优化的下一代渲染,有更合理的架构设计以及更好的性能。大体部分在2021年基本完成,可以看这里文章的介绍open in new window

NG节点的更新主要是因为之前的节点包含了输入、输出还有布局算法的信息,也就是说单个节点可以看到整棵树的状态(节点有可能需要获取父节点的宽高数据,但是父节点正在递归子节点布局中,实际上还没确定最后的布局)。

而新的NG节点对输入和输出做了明显的区分,而且输出是immutable的,可以缓存结果

布局结果指向描述物理几何的片段树,如图一个NGLayoutResult对应几个NGPhysicalFragment,对应右上角的几个矩形图形,如果NGLayoutResult没变化则对应整块都不会变化。

举一个布局例子

上图的HTML代码,会渲染如右下角的例子,对应的DOM树如左侧所示

DOM树跟Layout树很像,节点几乎是一一对应的,但是注意这里anonymous匿名节点被创建出来是因为元素混合了不同布局类型(又有block又有inline)。一个布局节点下的子节点只能是块级元素或者内联元素两种类型的其中之一,这样设计是为了高效处理不同布局类型

fragment tree里我们可以更好的看到文本换行后的绘制结果,以及每个fragment的位置和大小

Paint

paint阶段只是创建绘制指令paint op,页面还没有东西,甚至直到GL调用之前页面都是没有呈现任何东西的状态

创建绘制指令列表

绘制paint阶段创建绘制指令列表paint ops list

绘制指令paint op可以理解为在某些坐标(x,y,w,h)用什么颜色(color)画一个矩形类似的意思

每个布局对象LayoutObejct可以有多个DisplayItem,对应于其视觉外观的不同部分,如背景、前景、轮廓都是一个DisplayItem

样式可以控制绘制顺序

正确的绘制顺序非常重要,这样当元素重叠时,它们才能正确堆叠。顺序可以由样式如z-index控制,而不是完全依靠DOM的先后顺序。

所以z-index就是一个绘制指令的排序权重标识,可以简单测试一下

按照层叠上下文解释,理论上只有下面三种文档流,用z-index>0绝对定位测试一下

  • 正常文档流
  • 浮动
  • 绝对定位

DEMOopen in new window

绘制分不同阶段进行

每个绘制阶段paint phase都需单独遍历堆叠上下文staking context

一个元素甚至可能部分位于另一个元素的前面,部分位于另一个元素的后面。这是因为绘制在多个阶段中运行,每个绘制阶段都对自己的子树进行遍历。

举一个绘制例子

如上,一个样式和DOM节点渲染出来的结果,包含了四个绘制指令paint ops。

  1. document背景色绘制
  2. 块级元素的背景色绘制
  3. 块级元素的前景色绘制(包含文本的绘制)

文本块的绘制

文本绘制操作包含文本块的绘制,其中包含每个字的字符和偏移量以及字体。如图这些数据都是HarfBuzz计算后得到的

raster

中文说的栅格化或者光栅化,本文取PS图层右键的栅格化为译文。熟悉PS的会知道矢量图形栅格化后放大图形会“糊”,但是不做栅格化处理直接放大矢量图形则不会。原因就是栅格化后只记录了单像素点的rgba值,放大后本来一个点数据要填满N个点,图像就”糊了“

raster将绘制指令转化为位图

把显示列表里的绘制操作执行的过程,成为任务,也称栅格化。比如PS里的合并图层任务,主要区别就是本来矢量的图任务后会变成位图bitmap,后面再缩放就会模糊。

生成的位图bitmap中的每个单元格都包含对单个像素的颜色和透明度进行编码的位。这里用十六进制FFFFFFFF表示一个点的rgba值

raster还可以对页面图片进行解码

任务还可以对页面中嵌入的图像资源进行解码。绘制指令引用压缩数据(JPEG、PNG等),任务调用适当的解码器将其解压缩。

GPU加速栅格化

GPU还可以运行生成位图的命令(“加速栅格化”)。请注意,这些像素还没有出现在屏幕上!
raster产生的位图数据存储在GPU内存中,通常是OpenGL纹理对象引用的GPU内存。

过去通常是存在内存里再传给GPU,但是现代GPU可以直接运行着色器shader并在GPU上生成像素,这种情况称为“加速栅格化”。但是两个结果都是一致的,最终内存(主存或者GPU内存)里拥有位图bitmap

raster通过Skia发出GL调用

GL调用即OpenGL调用,OpenGL意为"开放图形库",可以在不同操作系统、不同编程语言间适配2D,3D矢量图的渲染。

raster通过名为Skia的库发出GL调用。Skia提供了围绕硬件的抽象层,如路径和贝塞尔曲线,子像素抗锯齿以及各种混合叠加模式。

Skia是开源的,由谷歌维护。跟随Chrome一起发布,但位于单独的代码库中。它也被其他产品使用,比如Android。Skia的GPU加速代码路径构建自己的绘制操作缓冲区,在栅格化结束时刷新。实际上发起GL调用的是Skia的后端,后面会说到

raster运行在GPU进程中

回想一下,渲染进程是一个沙箱环境,因此它不能直接进行系统调用。绘制操作被运送到GPU进程进行任务处理。GPU进程可以发出实际的GL调用。除了独立于渲染器沙箱之外,在GPU进程中隔离图形化操作还可以保护我们免受不稳定或不安全的图形驱动程序的影响。比如GPU进程崩了,浏览器可以重启GPU进程

绘制请求通过命令缓冲区传递给GPU进程

栅格化的绘制操作通过GPU命令缓冲区command buffer传输,渲染进程和GPU进程通过IPC通道发送。命令缓冲区command buffer最初是为序列化的GL图形命令构建的,类似一个proxy。当前的“进程外”栅格化(即GPU)以不同的方式使用它们,更多是绘制操作的包装器,就是命令缓冲区command buffer与底层图形API无关

不同操作系统调用不同的共享OpenGL库

GPU进程中的GL函数指针通过动态查找操作系统底层共享的OpenGL库进行初始化,Windows上用Angleopen in new window做一个转化步骤。

Angel是另一个由Google构建的库;它的工作是将OpenGL转换为DirectX,DirectX是微软在Windows上用于加速图形的API。测试发现Angle比Windows的OpenGL驱动程序运行更好。

简版渲染流水线

至此拥有了整个pipeline,从DOM一直到内存中的像素,牢记渲染不是静态的,也不是执行一次就完成了,浏览器会话期间发生的任何事情都会动态更改渲染的过程。并且整个pipeline从头开始运行是非常昂贵的,尽可能希望能减少不必要的工作以提高效率

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的形成

简单理解就是会动或者将来会动的部分

为什么要分层?

分层的作用在有动画时候可以显著提升性能,如图所示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图块。图块大小根据不同设备的分辨率有不同的大小,比如256*256512*512

图块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 treeactive 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张图片到显卡的后缓冲区

至此浏览器完成了它的任务,底层驱动通过调用硬件完成绘制。最后,我们的像素出现在屏幕上

渲染流水线(全版)

回顾一下整个渲染流水线的过程,从渲染主线程获取Web内容,构建DOM树,解析样式,更新布局,layer分层后合成合成层,生成属性树,创建绘制指令列表。

再到渲染进程合成线程收到渲染主线程commit过来的带有绘制指令和属性树的layer,将layer分块为图块,使用Skia对图块进行栅格化,拷贝pending treeactive tree,生成draw quads命令,将quad发送给GPU进程的Viz线程,最后像素显示到屏幕上。

大多数阶段是在渲染进程里执行的,但是raster和display则在GPU进程中执行。

核心渲染阶段DOM,style,layout,paint是在渲染进程主线程的Blink进行的,但是滚动和缩放等交互在渲染主线程繁忙时可以在渲染进程合成线程里执行

渲染进程主线程

  1. DOM: 解析HTML生成DOM树
  2. style: 解析styleSheet生成每个结点的ComputedStyle
  3. layout: 生成layout tree,跟DOM树基本对应
  • 但是display:none的节点不显示直接裁剪,不会出现在LayoutTree
  • 伪类,伪元素是在LayoutTree生成的,不是在DOM上
  • LayoutEngine在重写,2021年中基本完成,更合理的输入输出分离架构以及更好的性能
  • 为了高效处理不同布局类型,一个布局结点下只能是块级元素或者内联元素两种类型的其中之一,某些匿名节点会被创建
  1. layer分层后合成(compositing assignments): 某些样式属性会单独形成层,如transform会形成单独的层方便进行图形变换,滚动元素会多出scrollbar的4层。合成任务在渲染进程的合成线程中执行,与渲染主线程隔离互不影响
  2. prepaint: 为了将属性与层解耦引入prepaint阶段,prepaint阶段需要遍历并构建属性树,属性树即存储如变换矩阵,裁剪,滚动偏移,透明度等数据的地方,方便后面paint阶段拿属性树数据处理
  3. paint: 绘制过程是将LayoutObject转化为绘制指令paint op,每个LayoutObject会对应多个绘制指令paint ops,比如背景,前景,轮廓等。样式可以控制绘制的顺序。绘制有自己的顺序,如背景色在前,其次是浮动元素,前景色,轮廓outline

渲染进程合成线程

页面的滚动等交互会进入渲染进程合成线程compositor thread里处理,这也是渲染进程主线程繁忙时交互也不卡的原因

  1. commit: 渲染进程合成线程将层从渲染主线程拷贝出两份层和属性树副本
  2. tiling: 栅格化整个图层成本大,渲染进程合成线程将layer分块后选择视口相近的图块tiles再进行栅格化成本小很多
  3. activate: 合成线程具有两个树的副本,pending tree负责将新commit的layer转到栅格化线程池里的栅格化线程处理好后同步到active tree
  4. draw: 栅格化所有变换后的图块之后,生成draw quads命令,包含多个DrawQuad的CompositorFrame,这是渲染进程最后的输出,此时屏幕还没有像素出现

GPU进程

  1. raster: 栅格化是将绘制指令paint op转化为位图bitmap的过程,转化后每个像素点的rgba都确定。栅格化还处理图片解码,通过调用不同解码器解压缩图片,GPU可以加速栅格化,通过调用Skia对图块进行栅格化
  2. Skia: 封装OpenGL调用,提供异步显示列表,最后传递到GPU主线程处理,GPU主线程的Skia后台发起真正的GL调用
  3. display: GPU Viz线程里的显示合成器display compositor合并多个进程的CompositorFrame输出,并通过Skia发起图形调用,像素呈现在屏幕上

Canvas和WebGL

可能有的人听到过Canvas和WebGL,那么这两个是如何在浏览器跑起来的呢?

看一下MDN的对contextType定义open in new window,有这么几种

  • 2D二维只有xy 对应CanvasRenderingContext2D,用于提供canvas标签2D渲染上下文接口,用于绘制形状,文本,图像和其他对象,一般简单的canvas在这里
    • 简单例子如下
      const canvas = document.getElementById('my-house');
      const ctx = canvas.getContext('2d'); // 获取2D渲染上下文                  
      
  • 3D三维有xyz,一般称为webgl
    • webgl: 对应底层的WebGLRenderingContext,用于提供canvas标签提供OpenGL ES 2.0渲染上下文接口
      const canvas = document.getElementById('myCanvas');
      const gl = canvas.getContext('webgl'); // 获取3D渲染上下文                  
      
    • webgl2: WebGL2.0版本,对应底层的WebGL2RenderingContext,用于提供canvas标签提供OpenGL ES 3.0渲染上下文接口

这些绘图API是如何运行的?

  1. 创建ImageBuffer,将绘图API转化为内存buffer的操作
  2. createOffscreenContext创建对应宽高的离屏渲染上下文
  3. 转化为Skia的SkBitmap
  4. 调用Skia的canvas进行绘制矩形操作

可以发现其实对canvas的调用,就是通过调用浏览器封装好的Skia API实现的,浏览器实现封装实现类似OpenGL标准的接口对外提供调用。但是浏览器底层也依赖各种OpenGL图形库的实现。因为省略了浏览器渲染主进程DOM,style,layout等等步骤,所以canvas/webgl的效率很高,取而代之的是如果你需要实现一个类似网页的排版,你需要自己去计算所有元素的位置。

比如我之前在做游戏的时候,需要实现一个很简单的排行榜列表,类似下图,就是一个list需要居中然后排列前N个人的昵称和得分,但是因为游戏没有排版引擎,不能通过写CSS实现。需要自己去算每个元素出现的位置,间距等最后呈现出来

因为省略了排版的过程,所有算出来元素的 x,y,width,height 就是最后呈现的位置,当时就在想为什么Web有排版引擎而游戏没有
如果游戏有大量类似Web的排版操作,那么你会疯掉的。而网页如果有很多类似游戏位移(x,y),你也会疯掉的。本质是有无排版引擎对开发者实现思路的影响。诚然x,y操作简单灵活,但是不够系统,而排版引擎系统却不灵活简单。

参考

Last Updated:
<manfred>峯</hu>
欢迎关注微信公众号 【Big前端】无广告,无软文,就是这么傲娇。直推一线大厂高质量内容,不局限于前端·后台·运维相关,还包括房价🏠、信用卡💳等内容也可内推一线大厂腾讯阿里字节,对腾讯字节比较熟悉,简历可以发给我,我会给你介绍一线大厂的情况,让你更加了解一线大厂