渲染的理解 渲染怎么理解

文章来自https://mp.weixin.qq.com/s/9lX8VgMmtXRjFdf7zkOFMQ, @大前端

说到Chrome,浏览器不得不说Chrome是谷歌发布的商业产品,而Chrome是Chrome的开源版本,两者类似但不完全相同。

这里我尝试结合自己的理解和下面的PPT,用最直白的语言记录下最近学到的浏览器渲染原理的知识,方便后续参考。因为涉及的知识点实在太多太复杂,如有不足之处请见谅,如有错别字/误解请联系我指正。

为什么要这么做?这几年浏览器更新挺大的,Chrome/Chromium整体还在进化,越来越频繁。这篇文章有一些自己的理解。如果有任何错误,请联系我们。

整个PPT内容来自Chromium开发者Steve Kobes在PPT address的演讲。

https://docs . Google . com/presentation/d/1 bopxbgnrtu 0 ddsc 144 rcx ayga _ wf 53k 96 imrh 8 MP 34y/edit # slide=id . ga 884 Fe 665 f _ 64 _ 1691

分层架构:简单来说,浏览器作为一个应用,有content,Blink,V8,Skia等。最底层,像娃娃一样一个个引用。与普通的应用项目相比,意味着不断使用第三方库和组件来拼凑应用,Chrome也不例外。

内容可以理解为Blink渲染引擎,网页内容的一部分,会随着不同的网页而变化,除了浏览器主进程中的书签导航。它应该被称为网页的排版引擎。现有的Chrome/Edge作为开源项目使用和维护。在渲染过程中,Blink嵌入V8 JavaScript引擎执行JS代码4b5be6020a9e43a9a8aa840f624046bc?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=pa5Rea%2FiIN4p4QOZr%2BEFTITPbFc%3D&index=0

7eaa3107bb314ae1910150150ae3bf04?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=nF73JE5FrfdD6kozMKMYrZQhCn4%3D&index=1

渲染网页的渲染可以表示为内容渲染后最终呈现的过程,即代码交互页面。

什么是内容?077dedab108f4a39916fccc76a671276?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=rdRwN15lVLvDWusHIhQ2sk5Hlgg%3D&index=2

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

渲染进程渲染进程是一个沙箱。出于安全考虑,如果渲染进程单独挂起,也不会导致整个浏览器挂起。

渲染过程render process包括Blink渲染排版引擎和Chromium合成器(上图中绿色的CC缩写)。

ffaf2dee97b04108860974c85e4f8dbb?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=Y05VH7yc3U9mZHJfU3w7DV9kva8%3D&index=3

38589c3039e746e0a9108cd2b68fea01?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=KCMzAULMR9Ywtiiyyp8sFC1S4UM%3D&index=4

内容也表示网页内容的代码,如HTML、css、JS、图片等。以及视频、画布、WebAssembly、WebGL等。可以在内容区域显示或运行。

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

像素是如何呈现的?852e7b5e63894f40ae570e9a435ac709?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=RPzpoTw1%2BQdmpxaqjpai6%2F0yE6k%3D&index=5

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

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

所以渲染管道的整个过程就是把输入的HTML,CSS,JS转换成OpenGL调用,最后在屏幕上渲染像素。

浏览器渲染目标275d3085ccf54aafa7bad61da87f01ab?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=k93Mm%2FxFyS%2Fggso43V1%2BNQssAW0%3D&index=6

在第一次渲染中,网页内容被转换为底层OpenGL调用,以显示页面更新渲染。在JS运行、用户输入、异步请求或滑动等交互干预后,页面为了交互目的再次渲染,这里的重新渲染需要高效执行。你会想到缓存,对吗?是的,每个阶段的结果都被缓存,以提高渲染效率。JS API会查询一些渲染数据,比如一个DOM节点的信息。分割渲染阶段7a10e86d4dda484b86d328a912c7364d?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=V7ZMVhfGJUA5BLNEyIp3VxSbW9s%3D&index=7

如果将渲染管道分为多个阶段,可以看出,原始内容内容会被每个阶段stage处理成中间数据,最终呈现为图片。

36d99b241f5344ed8e70940f36a28ef9?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=QaH5sHZ4z9NrLkj4oiRz30E8i80%3D&index=8

将DOM解析为DOM 659bb92af0284d0cb3ffe49734ef4c01?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=V3i2%2FAuFKtqp88UFsLhSk98GIoA%3D&index=9

57a5d204cf51401fa7113b1d8ccab02f?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=FdMITQv4fyEK0SeAnXH9vtp67Fw%3D&index=10

HTML嵌套解析,解析是通过映射数据对象来完成的,以反映这里的嵌套模型。

DOM(文档对象模型)是一棵树。树有父子邻居关系,这个树把API暴露给JS调用。JS可以查询和修改这个树。JSV8引擎将DOM包装成DOM API,供Web开发者通过绑定系统调用。

一个文档可能包含多个DOM树d451c3d0415f4813ab7972e0917c0942?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=Ni9lxS%2Bqkq597c9AFmNMgnfonug%3D&index=11

782b5abc8b4f4c1aaad88153841fde65?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=2zhRZ4fJjoJIN1JSIL0i1rP1XlE%3D&index=12

如上例所示,自定义元素custom element有shadow tree。ShadowRoot的子元素实际上是嵌入在slot元素中的,这与前端框架的slots非常相似。

最后遍历树后合成视图,即两棵树合并成一棵树。

款式9c49c21c0c14415f9c5172a4a6623d1a?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=AAZuNjmFZ%2FNcVHrmSZuIq4U6SIU%3D&index=13

9ce15e4f433a44c2a2d16fae4981f34c?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=QVvJnlWBrsH2F%2BbNyCwUkesJwr8%3D&index=14

样式的步骤取决于前面的DOM树的分析结果,选择器选择DOM节点集来确定最终的应用范围。样式的最终效果是多个选择器共同作用的结果,样式之间可能会相互冲突,导致不能按预期运行。对选择器优先级感兴趣的同学可以自行查阅。

dff1fb5570f5468cb3c20760a51bf822?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=bHNEqI1j2vu1dc1UM%2ByL9XMDrVI%3D&index=15

CSS解析器样式表样式表构建样式规则。样式表可能位于style元素的css文件中,是单独加载的资源,或者默认情况下由浏览器提供。规则以各种方式被索引以实现高效的查找。该类由Python脚本在构造时自动生成,所有样式属性都是以声明方式定义的。如上图所示,右上方的css_properties.json被py脚本转换成. cc文件。

计算类型8cf04af68e45498a963837576e22088d?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=L7R69TRUWfDUtyRWdbLiKq3a0wk%3D&index=16

8a132d2388ad47bc95465bff85f7da37?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=A1Pg76aBT7o3RWEdfN0r130XPOc%3D&index=17

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

在Chrome浏览器中,是开发者工具的计算样式属性对应的列。或者通过getComputedStyle的JSAPI获取。

布局218f7b831f9a44508a5b143c1181fd52?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=GlmsbDBFx9Bm4Sl3Bfh24uiV5RY%3D&index=18

计算完DOM和Style之后,开始布局阶段,比如把DIV解析成一个块级LayoutReat区域,使用x y width='360px 'height='auto'/

表示,布局是计算x,y,width=' 360px 'height=' auto'/

,高度这些数据

ddafda39daae4185be52c33318863b61?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=Ka5LmqXoP4qVfUu6zibSBdjO5ks%3D&index=19

默认情况下,文档是按照文档流的顺序排列的。

e11cb7ed40fb4bf7b07a693d9f6b22f8?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=8OPzB9yEc1HFXt2uNxeoiT0NO8w%3D&index=20

文本和行内元素左右浮动,行内元素被行尾打断(自动换行)。也有从右向左的语言,如阿拉伯语和希伯来语。

7dfd027d4a9b4311995202d5e3d87457?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=H%2FoJo%2BfOZz%2FkeOzJ4hOLC4yx5Rc%3D&index=21

布局还包括字体的排列,因为布局需要考虑文字在哪里换行。Layout使用一个名为HarfBuzz的开源文本库来计算每个字形的大小和位置,这决定了文本的总宽度。在字体形成中,必须考虑到排版特征,如字母间距和连字符。

649adc1462034a04a6c5a7f70dec2e8b?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=bxyR5L8GrnWynG0kSdL8h5M2QYs%3D&index=22

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

17f5e0c0d413443e9d6e44503e488c93?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=FYQz4SpUbxUh7lZKSeSSI%2FQuk74%3D&index=23

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

f48b26e252394523a772444b35302424?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=MU6uRSM1n8yUVDHPOFZHtRt95kY%3D&index=24

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

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

DOM节点不一定与布局节点一一对应5d10c780e1f04d839dc9b8d2f0bd1740?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=WWkiuxIvLDZttQNC4bdbB2i9ZuE%3D&index=25

一般来说,一个DOM节点会有一个LayoutObject,但是有时候LayoutObject没有与之对应的DOM节点。比如上图,span标签外没有嵌套section标签,但是LayoutTree会自动创建LayoutBlock的匿名节点与之对应。例如,如果样式具有显示样式:无,则不会创建相应的布局树。

最后,如果它是一棵shadowTree,它的LayoutObject节点可能在不同的TreeScope中。

布局正在重写2b8090432fc347638fc21f4a8e5467e5?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=pG8Fm4PCjMvVe4Jh%2BLqj1XdsZLs%3D&index=26

如上图所示,「LayoutNG」代表下一代布局引擎。2020年布局引擎还在过渡阶段,所以有中间形态。上图包含了LayoutObject和LayoutNGMixin的混合节点。所有未来的节点都将成为LayoutNG的LayoutObjects。

9bceaa2549794888abb873bc0fe028b3?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=w0T0ycwUM0jywohcOHcAfi0xh3g%3D&index=27

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

f3591583e1cc4e9eab9a0b4623e65727?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=7sUB3V2sD0E7pYMvOIyilyNRR9Q%3D&index=28

新的NG节点明确区分了输入和输出,输出是不可变的,可以缓存结果。

ee0a92d7f7b941ae8073535d0fa94554?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=tSJ25Dha3I4R3MT4uCTqac4edE4%3D&index=29

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

以布局为例bab0153e7bd4462bbeb15f789f680eee?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=HHp%2FHlunbdnaVmeUI8SFbgJUfAI%3D&index=30

上图中的HTML代码会被渲染成右下角的例子,左边显示的是对应的DOM树。

e19389d7d9c14f8caa2a4257d233912b?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=rK6bIz11%2BaMZY2P3XoXA9zG3%2FNk%3D&index=31

DOM树和布局树很像,它的节点几乎是一对一的,但是注意这里创建了匿名节点,它只有一个块级的子元素。布局节点只能有一个块级元素或内联元素。

图中的前两个子元素实际上共享匿名LayoutNGBlockFlow,这意味着它们有一个共同的父节点。

8b164c5d387945e99d09136236523683?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=LjtToiEw3QHSO0sT3ZzIhxt%2FrN4%3D&index=32

cf5ce097e9d34ddd8b2bfbe9ad4fd6e7?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=ReAs7VW9mYHfYQEwXiZmJ0aViH0%3D&index=33

「fragment tree」中,我们可以更好地看到文字换行后的绘图结果,以及每个片段的位置和大小。

Paintpaint stage只是创建绘图指令paint op,页面上没有任何内容。即使在调用GL之前,页面也处于空的状态。

创建绘图说明列表751a6a9782944c2d820cc9c923c28979?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=h%2Bw780cqjPEQucRlZsePh4YJN18%3D&index=34

油漆阶段创建油漆指令列表油漆操作列表

绘图指令paint op可以被理解为在特定坐标中用什么颜色绘制矩形的类似含义,

每个布局对象LayoutObejct可以有多个显示项,对应于其视觉外观的不同部分,如背景、前景、轮廓等。

样式可以控制绘制顺序7be208d1f3f84d8e86c868afdba7191d?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=iNHL5aSRyhSaInGHVG%2F2l1yZSI4%3D&index=35

的正确绘制顺序非常重要,以便当元素重叠时,它们可以正确堆叠。可以通过样式来控制顺序,而不是完全依赖DOM的顺序。

不同阶段的绘图5d91fb7d452d4273b917575308fa9f8f?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=WZeY3FI6%2BrcLNGF3%2BsWDAVuOPuU%3D&index=36

每个绘图阶段「paint phase」需要分别遍历定位上下文。

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

举个画画的例子。

etail&x-expires=1702403881&x-signature=cxGlTufBwn1gBvTOzn%2BNA32H%2FOM%3D&index=37″ />

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

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

97915c1d64fa40269f1abcd88f7bc39c?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=I7pcb%2FDjWZggmwZoj3ptVJ%2FdbVM%3D&index=38

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

raster

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

raster将绘制指令转化为位图

1c53c11573644b9a8982e527a33ce1d3?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=bZLOfizgSzaUHUMHHcScDQNWVi0%3D&index=39

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

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

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

f2f3fcd90a114efa88ef4d27510c5953?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=fjWfoiVHH0Ahmzb089MrFBc%2F87M%3D&index=40

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

GPU加速栅格化

b5d46dc4d99b4f1687cfc21fde4135f1?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=qEN9rMklCB7L7B7e3M0la88HCVI%3D&index=41

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

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

raster通过Skia发出GL调用

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

26d99d0f065549b3ab5a1f9e62532471?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=BTeymJzZ%2BjuUyoTcOPRn5rFf5cw%3D&index=42

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

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

raster运行在GPU进程中

02b86a3892a14e8aaf3fcb708b64dc85?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=PhZ6kwoaRsdnbX5%2Fs%2F%2BcSbcL3Y4%3D&index=43

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

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

2838a37eb66941b085e2a659156d847a?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=s36X3g%2B1hXkjGmE9BCePiO8fw30%3D&index=44

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

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

eee4001db6734b7ca935f58eeaf76e5d?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=wDvE0qkCDw1VeKepmlU189r1T80%3D&index=45

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

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

渲染流水线(简版)

88268ae1877c4aacb911a170c73b551c?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=wQXTD%2FUpC2N1NM4cLoCh0jhg7Yc%3D&index=46

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

frames动画帧

d497591ce66c47f688d1cbf154835c08?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=z9lGAp9POtirREUY0NI76zPHdCA%3D&index=47

低于60帧每秒的动画和滚动看起来会非常卡,渲染器生成动画帧,每个帧都是内容在特定时间点状态的完整呈现,多个帧连起来就是看到的动画,其实动画只要达到60帧每秒那么看起来就会是连贯的。新的设备甚至要求90或120甚至更高的帧率。

如果在1/60秒内,约16.66ms还不能渲染完一帧画面,那么画面看起来就是断断续续很卡的样子

流水线各个阶段都依赖上一步的结果

da47c2abd74c49fe8fc208011fed1b41?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=DJjv4r8ppIDAoqAZyY%2F4zKkU39s%3D&index=48

为了提高性能,很简单的想到了尽可能复用上一阶段处理的结果,对于渲染来说既重复使用以前帧的输出

重绘

c8f64532865f4296b1614806ac39e521?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=8hLTAyP%2FFIyzeyTVAA%2BLd%2FeDiVM%3D&index=49

大块区域的绘制和栅格化是非常昂贵的,比如在滚动的时候,视口内所有像素都变化了,这个过程称为重绘repaint

渲染进程主线程的竞争关系

748ac2c845904a38ae93963dbf468d09?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=6eB2loJjCcXQVr2BKD8c2P5yWgA%3D&index=50

渲染进程的主线程的任何事情都会跟JS竞争(互斥关系),意味着其实JS也会阻塞渲染主线程其他任务的执行

分层与合成线程

40c05f0d1e74426ca6b4e15c66a0cad3?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=cBZW8dbMc6EpNjzJfryt%2B6yCco0%3D&index=51

将页面分解为不同的层便于栅格化raster对不同的层单独处理,在渲染进程主线程构建层后commit到合成线程compositor thread去,合成线程compositor thread会对每一层进行单独绘制

我们可以在浏览器开发工具的Layer看到当前页面的分层,分层的目的是可以对单独的层进行变换transform和栅格化raster

试想一下如果有123三层,其中1,2两层没变化,第3层旋转了,那么只要对第三层每帧进行变换就可以得到每一帧的输出,计算量大大减少

「所以分层的目的是为了减少计算加速渲染效率,在渲染进程合成器线程执行则是为了不影响渲染主线程的任务执行」

图中的impl*即渲染进程的合成线程,因为历史原因在代码里都是这样表示,后面所有表示合成线程都用impl表示

为什么要分层?

125473c6ae31440389d0bdb66c70d5bf?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=YdUKlj5ML7ep0PDKKO6snUe%2BZyI%3D&index=52

分层的作用在有动画时候可以显著提升性能,如图所示BBB文本一层的变换不会影响其他层

58f6ae09d2fd464f93ee760ea4e97583?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=LGRHBP2OovqFoXkCjuQlxcca9is%3D&index=53

4d18320218d54ce1b880c8ec350dc982?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=FuCR5hKzgsKl7YMI5%2FhJZIBlhr8%3D&index=54

动画是层的移动,页面滚动是层的移动和裁剪,放大缩小也是层的缩放

合成线程处理输入

5167e187a72b4d71aaac7b976946888f?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=JwCvlXrzF3g5N57Bp478AfebIdI%3D&index=55

当滚动事件没有触发JS逻辑时候,即使渲染进程主线程很繁忙,但是浏览器进程发出的页面滚动事件的处理也不会受到影响,因为渲染进程的合成线程compositor thread可以单独处理滚动事件

当然如果滚动触发了JS的逻辑,那么合成线程必须转发事件到主线程去,滚动事件会进入主线程任务队列等待处理

层的提升

15f6b0121955400da8078803823bcf90?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=%2FI49EPJJM%2FbChGlmx4AOC%2FE8GlQ%3D&index=56

正常情况下一个LayoutView会创建一个PaintLayer,对应一个cc(Chromium Compositor) Layer。但是某些样式属性也会导致对应的LayoutObject单独成层,比如transform属性就类似创建新层的“触发器”一样,浏览器遇到这个属性就会单独创建新层,cc(Chromium Compositor) Layer没有父子关系,是一个平级的列表,但是还是保留LayerTree的名称

滚动容器创建多个层

f7e0d2f43508449c9850b68e22352202?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=ZsJCed8rkmyjhxj8aQjpaApr2UA%3D&index=57

滚动容器创建特殊的多个层,比如元素加了overflow:scroll的滚动属性,那么合成的时候会有5个层,其中4个层都是滚动条scrollbar的层,这些层合并起来称为CompositedLayerMapping

394ab889e51b402dacd5c5c125fe96c0?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=KmGo6hPmHmAlupEZyzcZ9mAw1Z4%3D&index=58

合成透明滚动条会禁用子像素抗锯齿,如上图左下角所示。而且判断是否合成滚动条也有判断逻辑,在安卓和ChromeOS上可以合成所有的滚动条

合成任务

e8ab9663aa364b7c8fc41bb70c325811?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=XjpgiSpIhLKmo0Rc%2Fiix7pTqqg0%3D&index=59

如上图,合成任务包含构建层树的过程。在布局layout之后,绘制paint任务之前,这个过程也可以称为「分层和合成任务」,每一层layer都是独立绘制的,一些属性节点单独为层,比如will-change,3D属性transform之类

prepaint构建属性树属性树

6a4ee27cc92e433bbe8a842ecd198c3d?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=2%2BY8ev0tuhuapvoJH5PlJgKMYcE%3D&index=60

渲染进程合成线程绘制的时候,合成线程里的合成器可以将各种属性应用于其绘制的图层,如变换矩阵,裁剪,滚动偏移,透明度。这些数据储存在属性树里,可以将这些理解为图层的属性(过去也是这么干的)。后面为了解耦这些属性,让它们可以脱离层单独使用,需要引入prepaint的过程

e66e926695a9433c84f72e41ac301cbc?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=PCFT39ddsuccU%2BiiVbT1ll1Lifg%3D&index=61

预绘制prepaint阶段遍历并构建属性树

合成后绘制(CAP)

CAP是composite after paint的缩写,它的目标是将属性和层解耦。即在paint阶段只需要paint的信息,而不需要知道层的任何信息,因为这时层还没有构建

11adb32e25f0421fabdba57ddb685fd0?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=Ow79vebZT4jfJMtTHOP05nEQtpE%3D&index=62

在过去,变换、裁剪、效果滚动等信息等存储在层本身上,但CAP要求层的属性解耦。未来,层layer的合成会在绘制后进行

commit复制层数据到合成线程

292555cc74084734bbc71d39d4fa4364?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=3eRfi%2B794CgEjdA%2Bj7ict3xxNy0%3D&index=63

在绘制paint阶段完成后,即绘制指令准备完成后,会进入渲染进程合成线程commit阶段

commit会拷贝层和属性树生成副本,这里合成线程的commit会阻塞主线程直到commit完成

「注意:渲染进程合成线程拿到的是layer副本,用LayerImpl表示」

tiling分块平铺

e6dd31cc8cb74618a50ccf28a4adf875?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=xmAJ90tX1DIcDgyqjCDFyVNP0R0%3D&index=64

整个网页是非常大的,向下延伸理论上可以无限长(比如新闻类网站的无限滚动)。

栅格化是绘制之后的步骤,栅格化会将绘制指令转化为位图bitmap。试想一下如果在绘制完整个图层之后再栅格化整个图层,则成本会很大,但如果只栅格化部分图层的可见部分成本则会小很多。

这里tiling是平铺的意思,类似装修时候铺地板用大块瓷砖平铺,页面显示的做法类似。

根据视口viewport所在位置的不同,渲染进程合成器线程会选择靠近视口的图块tiles进行渲染,将最后选择渲染的图块传递给GPU栅格化线程池里的单个栅格化线程执行栅格化,最后得到栅格化好后的tile图块。图块大小根据不同设备的分辨率有不同的大小,比如256*256或512*512

「图块tiles是栅格化任务的单位,栅格化就是将一块块的tiles转化为位图bitmap」

根据分块tiles去绘制层

fa7213edf4354cdc8536e432cf123a5e?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=H7bUa1gY9q61ATb49oLimioWH3E%3D&index=65

在栅格化所有的图块tiles完成后,渲染进程的合成器线程会生成draw quads命令。

quad类似于在屏幕上特定位置绘制图块tile的指令,draw quads就是绘制图块们的意思。

此时的quad是层树layer tree在拿属性树经过一堆变换后的最终结果,每个quad都引用图块tile在GPU内存里的栅格化输出结果。

多个DrawQuad最后被包装在「CompositorFrame」里(简单理解就是一排要铺上去的瓷砖 :-),这是渲染进程最后的输出,包含有渲染进程生成的动画帧,会被传递给GPU进程。

「注意执行到这里还只是数据,这里屏幕还没有像素呈现」

activation

04155db208424011a6ff052c8b87ab7c?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=UOLAku8zSGEqmZePHefw8GkJLA8%3D&index=66

在准备图块tiles进行栅格化和draw两个阶段渲染进程的合成线程都会参与,但是渲染进程主线程里的layer数据还在不断commit过来。实际上合成线程具有两个树的拷贝副本

「pending tree」: 负责接收新的commit并转给栅格化线程池里的栅格化线程执行,完成后进入激活activation阶段,同步复制处理好后的layer副本到active tree里「active tree」: 绘制上一次activation同步复制的layer副本(来自上一个commit)

这里pending tree 和 active tree都是层列表和属性树的结合,不是真的树结构,基于习惯沿用树的叫法

display(Viz)

e1a24890bfe74ab7bd6e7a03aaef2a2f?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=uYYXvPCrSGY9BkErnCHgfUgZo8I%3D&index=67

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

d092d32d200a47839af375b89f6a2891?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=M756pvlZiSLvu5AIChA9G7kyRRU%3D&index=68

Viz线程除了做界面聚合还发起图形调用,最后屏幕上显示compositor frame的quad。Viz线程是双缓冲的,分为前置缓冲区和后置缓冲区,这里将数据处理后序列化放到后置缓冲区

8cf5b54685804176825500434312401f?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=LLimcWADVOD8%2B77y4ZAPlnYCX84%3D&index=69

旧模式是GPU主线程解码器真正发起GL调用,新模式中是交给Skia库。Skia绘制到一个异步显示列表里,会一起传递到GPU主线程。GPU主线程的Skia后端发起真正的GL调用。

分离GL调用通过第三方的Skia或者未来准备使用的Vulkan实现与OpenGL解耦

前后缓冲区

在大多数平台上,显示合成器display compositor的输出是双缓冲的,即包含前后两个缓冲区。图块绘制到后台缓冲区,Viz发出命令交换前后缓冲区使其可见

也就是说屏幕显示器这一帧的画面,是每HZ从前置缓冲区读取后在屏幕显示的,后置缓冲区在马不停歇的绘制,通过前后缓冲区的交换实现新一帧画面的呈现。在OS X上,使用CoreAnimation做了一些稍微不同的事情

显卡的作用?负责将数据写到后缓冲区,写完后前后缓冲区互换。通常情况下显卡的更新频率和显示器的刷新频率是一致的,如果不一致则会发现视觉上的卡顿。大多数设备屏幕的更新频率是60次/秒,这也就意味着正常情况下要实现流畅的动画效果,渲染引擎需要每秒更新60张图片到显卡的后缓冲区

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

渲染流水线(全版)

fe1eb5108aad4148a5239ff564c918c4?_iz=31825&from=article.detail&x-expires=1702403881&x-signature=YHt%2FjUxvESJ9K9WAlay1KbTUj6I%3D&index=70

回顾一下整个渲染流水线的过程,从渲染主线程获取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发起图形调用,像素呈现在屏幕上参考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

渲染怎么理解

作者:神说要有光zxg来源:申光编程秘籍

本文转载自微信微信官方账号《神之光编程秘籍》。作者说要有轻zxg。转载本文请联系申光编程秘籍微信官方账号。

前端有html css、canvas、svg、webgl渲染技术可用。我们将综合运用这些技术来绘制页面。你有没有想过这些技术之间的区别和联系,它们和图形有什么关系?

本文将谈谈网页渲染技术的计算机理论基础。

人眼的视网膜具有视觉暂留机制,即它看到的图像会保持大约0.1s的时间图形界面就是根据这个原理设计的逐帧刷新机制。需要保证1s至少渲染10帧,这样人眼看到的图像才是连续的。

每帧显示一幅图像,图像由像素组成,是显示的基本单位。不同的显示器实现像素的原理不同。

我们想画各种形状,如矩形、圆形、椭圆形、曲线等。画好之后,我们需要把它们变成图像。关于图形绘制有一系列的理论,比如贝塞尔曲线就是曲线绘制的理论。将图形转换为图像的过程称为光栅化。绘制和栅格化这些图形的过程就是图形学研究的内容。

图形可以被缩放、平移、旋转等。这些都是通过矩阵计算来实现的,这也是图形学的内容。

除了2D图形,还应该绘制3D图形。3D的原理是将三维坐标的顶点连接起来,形成一个三角形,这就是建模的过程。然后,把每个三角形的面都贴上一张图,这叫纹理。这是一个三维图形,也称为三维模型。

3D图形也需要栅格化成二维图像,然后显示。这种三维图形的栅格化需要找一个角度来观察,就像拍照一样,所以这个概念一般被称为相机。

同时,为了让3D图形更加逼真,还引入了光的概念,即当一束光来的时候,3D图形的各个表面会发生什么变化,会如何反射等等。不同的材料有不同的反射方式,比如漫反射和镜面反射,所以有不同的计算公式。一束光会射到一些物体上并反射回来。这个过程需要一系列的追踪计算,这就是光线追踪技术。

我们也能感觉到,3D图形的计算量比2D图形大很多,用CPU计算很可能达不到1s > 10帧。所以后面出现了专用于3D渲染加速的硬件,叫做GPU。它专门用于这种并行计算。它可以批量计算一堆顶点、三角形和像素的栅格化。这个渲染过程称为渲染管道。

目前的渲染管道都是可编程的,也就是说,你可以控制顶点的位置和每个三角形的颜色。这两种类型分别称为顶点着色器和切片着色器。

总之,2D或3D图形被渲染和光栅化成帧并显示。

事实上,一些图像处理可以在它成为图像后进行,例如灰度、反色、高斯模糊等各种滤镜的实现。

因此,前端渲染技术的理论基础是计算机图形图像处理。

就前端和前端渲染技术而言,html css、svg、canvas和webgl都是用于图形图像渲染的技术,但各有侧重:

CSS用于图形布局,即计算文字、图片、视频等的显示位置。它提供了很多计算规则,比如流式排版非常适合图形排版,弹性排版便于自适应排版。但是,它不适合更灵活的图形绘制,因此将使用其他几种技术。

Canvas就是在给定的画布区域内,在不同的位置绘制图形和图像。它没有布局规则,因此很灵活,经常用于可视化或游戏开发。但是canvas并不保留绘制图形的信息,生成的图像只能在固定的区域显示。当显示区域变大时,不能随之缩放,所以会失真。如果需要无失真缩放,将使用其他渲染技术。

Svg会将绘制图形的信息保存在内存中,在显示区域改变后会重新计算。它是一个矢量图,通常用于绘制图标、字体等。

以上三种技术都用于绘制2D图形图像。如果要画3D内容,就得用webgl。它提供了用于绘制3D图形的API,例如通过顶点构建3D模型,映射每个表面,设置光源,以及光栅化为图像。它通常用于增强网站、3D可视化、3D游戏等的交互效果。通过3D内容,然后在虚拟现实中进行3D交互。

所以虽然前端渲染技术的底层原理是图形图像处理,但是上层提供的四种渲染技术各有侧重。

然而,它们仍有许多相似之处:

因为它们的基本图形原理还是一样的。

再说3D内容,也就是webgl内容,会用GPU计算,但是css也可以用GPU计算。这叫做css的硬件加速,有四个属性可以触发硬件加速:变换、不透明、滤镜、意志改变。(关于GPU和css硬件加速的更多信息,请阅读本文:这一次,彻底了解GPU和css硬件加速)

除了图形图像技术,html css还使用了编译技术。因为html和css都是DSL( domin specific language)的一种,也就是专门为界面描述而设计的语言。用html表示dom结构,用css给dom添加样式,只需要一点点代码,然后运行时解析html和css创建dom,添加样式。

DSL可以使特定领域的逻辑更容易表达。前端领域还有一些其他技术也使用DSL,比如graphql。

四种前端渲染技术:html css、canvas、svg、webgl各有侧重,分别用于渲染不同的内容:

但它们的理论基础是计算机图形图像处理。(而且为了方便表达逻辑,html css还设计了DSL,使用了编译技术。)

这四种渲染技术看似差别很大,但在理论层面,很多东西是一样的。这就是为什么我们要学习计算机的基础知识,因为它能让我们对技术有更深更本质的理解。

web3.0

OKX安卓客户端最新版下载指南与安全安装步骤

2026-3-27 12:04:05

好玩下载

桌面图标管理下载,桌面图标管家

2024-3-19 17:18:46

购物车
优惠劵
搜索