当前位置: 首页 > news >正文

真正理解浏览器渲染更新流程

浏览器渲染更新过程

文章目录

  • 浏览器渲染更新过程
    • 帧维度解释帧渲染过程
    • 一些名词解释
      • Renderer进程
      • GPU进程
      • rendering(渲染) vs painting(绘制)⭐
      • 位图
      • 纹理
      • Rasterize(光栅化)
    • 1. 浏览器的某一帧开始:vsync
    • 2. Input event handlers
    • 3. requestAnimationFrame
    • 4. 强制重排(可能存在)
    • 5. parse HTML(构建DOM树)
    • 6. 计算样式
      • 6.1 把CSS转换为浏览器能够理解的结构
      • 6.2 转换样式表中的属性值,使其标准化
      • 6.3 计算出DOM树中每个节点的具体样式
    • 7. 构建Render Tree(渲染树)的流程:
    • 8. Layout(重排reflow),构建布局树
    • 9. 分层、合成层(或者叫update layer tree)
      • 9.1 分层
      • 9.2 update layer tree
      • 9.3 补充解释:Render Object、Render Layer、Graphics Layer(又称Compositing Layer)和Graphics Context
        • Render Object
        • Render Layer
        • Graphics Layer(又称Compositing Layer)和Graphics Context
      • 使用 合成层提升 减少重绘重排
        • 使用transform和opacity书写动画
        • will-change
        • Canvas
    • paint(图层绘制),重绘
    • 分成图块 + 栅格化(raster)操作
    • draw
    • 引用

之前阅读过李兵老师的《浏览器工作原理与实践》,但还是对其中有些概念模糊,于是趁着国庆,对浏览器渲染更新原理进行梳理。本篇只是对一些优秀资料的总结及自己的理解,如果时间充裕建议阅读本文最后的引用。

先放图:

帧维度解释帧渲染过程

在一个流畅的页面变化效果中(动画或滚动),渲染帧,指的是浏览器从js执行到paint的一次绘制过程,帧与帧之间快速地切换,由于人眼的残像错觉,就形成了动画的效果。那么这个“快速”,要达到多少才合适呢?
我们都知道,下层建筑决定了上层建筑。受限于目前大多数屏幕的刷新频率——60次/s,浏览器的渲染更新的页面的标准帧率也为60次/s60FPS(frames/per second)。

  • 高于这个数字,在一次屏幕刷新的时间间隔16.7ms(1/60)内,就算浏览器渲染了多次页面,屏幕也只刷新一次,这就造成了性能的浪费。
  • 低于这个数字,帧率下降,人眼就可能捕捉到两帧之间变化的滞涩与突兀,表现在屏幕上,就是页面的抖动,大家通常称之为卡顿

来个比喻。快递每天整理包裹,并一天一送。如果某天包裹太多,整理花费了太多时间,来不及当日(帧)送到收件人处,那就延期了(丢帧)。
标准渲染帧:
v2-e893a683add1fe8e77e6e6d7676e2662_720w.png
在一个标准帧渲染时间16.7ms之内,浏览器需要完成Main线程的操作,并commit给Compositor进程
丢帧:

主线程里操作太多,耗时长,commit的时间被推迟,浏览器来不及将页面draw到屏幕,这就丢失了一帧

一些名词解释

Renderer进程

  • Main线程:浏览器渲染的主要执行步骤,包含从JS执行到Composite合成的一系列操作。负责解析html css 和主线程中的js,我们平时熟悉的那些东西,诸如:Calculate Style,Update Layer Tree,Layout,Paint,Composite Layers等等都是在这个线程中进行的。 总之,就是将我们的代码解析成各种数据,直到能被合成器线程接收去做处理。
  • Compositor(合成)线程:
    1. 接收一个vsync信号,表示这一帧开始
    2. 接收用户的一些交互操作(比如滚动) ,然后commit给Main线程
    3. 唤起Main线程进行操作
    4. 接收Main线程的操作结果
    5. 将图层划分为图块(tile),并交给栅格化线程
    6. 拿到栅格化线程的执行结果,它的结果就是一些位图
    7. commit给真正把页面draw到屏幕上的GPU进程
  • Compositor Tile Work(s)线程:Compositor调起Compositor Tile Work(s)来辅助处理页面。Rasterize意为光栅化。这里的 Tile 其实就是位图的意思(下文会详细说明),合成线程会将图层划分为图块(tile),生成位图的操作是由栅格化来执行的。栅格化线程不止一个,可能有多个栅格化线程。

GPU进程

整个浏览器共用一个。主要是负责把Renderer进程中绘制好的tile位图作为纹理上传至GPU,并调用GPU的相关方法把纹理draw到屏幕上。GPU进程里只有一个线程:GPU Thread。
这里其实只需要知道:GPU进程把 render进程的结果 draw 到 页面上。

rendering(渲染) vs painting(绘制)⭐

这里的 painting 也可以理解成上面的 draw,火焰图中也会出现这两个关键词。
Snipaste_2023-10-01_17-14-56.jpg
我们可以想象成 除了浏览器之外,还有一个后台工人,浏览器使用双缓冲,始终有两张图

  • rendering 渲染:后台工人画的过程,这里就是 浏览器的render进程
  • painting 绘制:当后台工人画好后往浏览器页面上放的过程,GPU进程负责将画好的东西paint(draw)到浏览器上

Snipaste_2023-09-28_22-51-36.jpg

后台工人先render一张,render完毕后,把浏览器的那张图替换下来叫paint(draw),然后后台工人又开始在替换下来的那张图上进行render
浏览器每一帧会替换一次,保证动画是连续的,很像动画那样一帧一帧

位图


就是数据结构里常说的位图。你想在绘制出一个图片,你应该怎么做,显然首先是把这个图片表示为一种计算机能理解的数据结构:用一个二维数组,数组的每个元素记录这个图片中的每一个像素的具体颜色。所以浏览器可以用位图来记录他想在某个区域绘制的内容,绘制的过程也就是往数组中具体的下标里填写像素而已。

纹理

纹理其实就是GPU中的位图,存储在GPU video RAM中。前面说的位图里的元素存什么你自己定义好就行,是用3字节存256位rgb还是1个bit存黑白你自己定义即可,但是纹理是GPU专用的,GPU和CPU是分离的,需要有固定格式,便于兼容与处理。所以一方面纹理的格式比较固定,如R5G6B5、A4R4G4B4等像素格式, 另外一方面GPU 对纹理的大小有限制,比如长/宽必须是2的幂次方,最大不能超过2048或者4096等。

总结:render进程中的叫位图,GPU进程中的叫纹理,生成位图(纹理)的这个过程叫栅格化,ok,过…

Rasterize(光栅化)


在纹理里填充像素不是那么简单的自己去遍历位图里的每个元素然后填写这个像素的颜色的。就像前面两幅图。光栅化的本质是坐标变换、几何离散化,然后再填充
同时,光栅化从早期的 Full-screen Rasterization基本都进化到了现在的Tile-Based Rasterization, 也就是不是对整个图像做光栅化,而是把图像分块(tile,亦有翻译为瓦片、贴片、瓷片…)后,再对每个tile单独光栅化。光栅化好了将像素填充进纹理,再将纹理上传至GPU。
原因一方面如上文所说,纹理大小有限制,即使你整屏光栅化也是要填进小块小块的纹理中,不如事先根据纹理大小分块光栅化后再填充进纹理里。另一方面是为了减少内存占用(整屏光栅化意味着需要准备更大的buffer空间)和降低总体延迟(分块栅格化意味着可以多线程并行处理)。
看到下图中蓝色的那些青色的矩形了吗?他们就是tiles。

可以想见浏览器的一次绘制过程就是先把想绘制的内容如文字、背景、边框等通过分块Rasterize绘制到很多纹理里,再把纹理上传到gpu的存储空间里,gpu把纹理绘制到屏幕上。
上面balabala说了一大堆,看得懂就看,看不懂就直接看总结…
所以,什么是光栅化,光栅化本质也是生成位图(纹理),不过会先分块,然后对每一块进行生成位图,这个分块的过程是由合成线程实现的,生成位图的过程是栅格化线程实现的。为什么要先分块,再栅格化,而不直接对整块屏幕做栅格化?为了减少内存占用和多线程处理(那这就意味着栅格化线程不止一个,可能有多个栅格化线程)。

名词解释完了,开始详细介绍浏览器渲染的每一步。再次摆出整个渲染流程图。

或者另外一张类似的流程图
9411794cdd8fcb055db322d71dcd8f17~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.webp

1. 浏览器的某一帧开始:vsync

Compositor(合成)线程接收一个vsync信号,表示这一帧开始

2. Input event handlers

Compositor线程接收用户的交互输入(比如touchmove、scroll、click等)。然后commit给Main线程,这里有两点规则需要注意:

  • 并不是所有event都会commit给Main线程,部分操作比如单纯的滚动事件,打字等输入,不需要执行JS,也没有需要重绘的场景,Compositor线程就自己处理了,无需请求Main线程
  • 同样的事件类型,不论一帧内被Compositor线程接收多少次,实际上commit给Main线程的,只会是一次,意味着也只会被执行一次。(HTML5标准里scroll事件是每帧触发一次),所以自带了相对于动画的节流效果!scroll、resize、touchmove、mousemove等事件,由于Compositor Thread的机制原因,都会每一帧只执行一次

3. requestAnimationFrame

window.requestAnimationFrame() 这个方法,既然已经说明了它是一个方法,那它一定是在 JavaScript 中执行的。

4. 强制重排(可能存在)

Avoid large, complex layouts and layout thrashing
下面对这个引用文章进行解释:
这里本来已经走到了我们熟知的浏览器渲染过程:
js修改dom结构或样式 -> 计算style -> layout(重排) -> paint(重绘) -> composite(合成)
首先运行 JavaScript,然后运行样式计算,最后运行布局。然而,可以使用 JavaScript 强制浏览器提前执行布局。这称为强制同步布局。
接下来解释 强制重排,也叫强制同步布局。
首先要记住的是,当 JavaScript 运行时,前一帧中的所有旧布局值都是已知的,可供您查询。因此,例如,如果您想在帧的开头写出元素(我们称之为“盒子”)的高度,您可以编写如下代码:

// Schedule our function to run at the start of the frame:
requestAnimationFrame(logBoxHeight);function logBoxHeight () {// Gets the height of the box in pixels and logs it out:console.log(box.offsetHeight);
}

如果您在询问框的高度_之前_更改了框的样式,则会出现问题:

function logBoxHeight () {box.classList.add('super-big');// Gets the height of the box in pixels and logs it out:console.log(box.offsetHeight);
}

现在,为了回答高度问题,浏览器必须_首先_应用样式更改(因为添加了super-big类),_然后_运行布局。只有这样它才能返回正确的高度。这是不必要且可能昂贵的工作。这就是强制重排。

强制重排意思是可能会在JS里强制重排,当访问scrollWidth系列、clientHeight系列、offsetTop系列、ComputedStyle等属性时,会触发这个效果,导致Style和Layout前移到JS代码执行过程中

浏览器有自己的优化机制,包括之前提到的每帧只响应同类别的事件一次,再比如这里的会把一帧里的多次重排、重绘汇总成一次进行处理。
flush队列是浏览器进行重排、重绘等操作的队列,所有会引起重排重绘的操作都包含在内,比如dom修改、样式修改等。如果每次js操作都去执行一次重排重绘,那么浏览器一定会卡卡卡卡卡,所以浏览器通常是在一定的时间间隔(一帧)内,批量处理队列里的操作。但是,对于有些操作,比如获取元素相对父级元素左边界的偏移值(Element.offsetLeft),但在此之前我们进行了样式或者dom修改,这个操作还攒在flush队列里没有执行,那么浏览器为了让我们获取正确的offsetLeft(虽然之前的操作可能不会影响offsetLeft的值),就会立即执行队列里的操作。
Snipaste_2023-10-01_09-26-23.jpg
所以我们知道了,就是这个特殊操作会影响浏览器正常的执行和渲染,假设我们频繁执行这样的特殊操作,就会打断浏览器原来的节奏,增大开销。
而这个特殊操作,具体指的就是:

  • elem.offsetLeft, elem.offsetTop, elem.offsetWidth, elem.offsetHeight, elem.offsetParent
  • elem.clientLeft, elem.clientTop, elem.clientWidth, elem.clientHeight
  • elem.getClientRects(), elem.getBoundingClientRect()
  • elem.scrollWidth, elem.scrollHeight
  • elem.scrollLeft, elem.scrollTop

更多会触发强制重排的属性:See more:What forces layout / reflow

5. parse HTML(构建DOM树)

如果有DOM变动,那么会有解析DOM的这一过程。
125849ec56a3ea98d4b476c66c754f79.webp

6. 计算样式

样式计算的目的是为了计算出DOM节点中每个元素的具体样式,这个阶段大体可分为三步来完成

6.1 把CSS转换为浏览器能够理解的结构

那CSS样式的来源主要有哪些呢?你可以先参考下图:
image.png
从图中可以看出,CSS样式来源主要有三种:

  • 通过link引用的外部CSS文件
  • <style> 标记内的 CSS
  • 元素的style属性内嵌的CSS
  • 和HTML文件一样,浏览器也是无法直接理解这些纯文本的CSS样式,所以当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构——styleSheets。
  • 为了加深理解,你可以在Chrome控制台中查看其结构,只需要在控制台中输入document.styleSheets,然后就看到如下图所示的结构


从图中可以看出,这个样式表包含了很多种样式,已经把那三种来源的样式都包含进去了。当然样式表的具体结构不是我们今天讨论的重点,你只需要知道渲染引擎会把获取到的CSS文本全部转换为styleSheets结构中的数据,并且该结构同时具备了查询和修改功能,这会为后面的样式操作提供基础

6.2 转换样式表中的属性值,使其标准化

现在我们已经把现有的CSS文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。
要理解什么是属性值标准化,你可以看下面这样一段CSS文本

body { font-size: 2em }
p {color:blue;}
span  {display: none}
div {font-weight: bold}
div  p {color:green;}
div {color:red; }

可以看到上面的CSS文本中有很多属性值,如2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
那标准化后的属性值是什么样子的?

从图中可以看到,2em被解析成了32px,red被解析成了rgb(255,0,0),bold被解析成了700……

6.3 计算出DOM树中每个节点的具体样式

现在样式的属性已被标准化了,接下来就需要计算DOM树中每个节点的样式属性了,如何计算呢?
这就涉及到CSS的继承规则和层叠规则了。
首先是CSS继承。CSS继承就是每个DOM节点都包含有父节点的样式。这么说可能有点抽象,我们可以结合具体例子,看下面这样一张样式表是如何应用到DOM节点上的

body { font-size: 20px }
p {color:blue;}
span  {display: none}
div {font-weight: bold;color:red}
div  p {color:green;}

这张样式表最终应用到DOM节点的效果如下图所示:

从图中可以看出,所有子节点都继承了父节点样式。比如body节点的font-size属性是20,那body节点下面的所有节点的font-size都等于20。
为了加深你对CSS继承的理解,你可以打开Chrome的“开发者工具”,选择第一个“element”标签,再选择“style”子标签,你会看到如下界面

这个界面展示的信息很丰富,大致可描述为如下

  • 首先,可以选择要查看的元素的样式(位于图中的区域2中),在图中的第1个区域中点击对应的元素元素,就可以了下面的区域查看该元素的样式了。比如这里我们选择的元素是

    标签,位于html.body.div.这个路径下面

  • 其次,可以从样式来源(位于图中的区域3中)中查看样式的具体来源信息,看看是来源于样式文件,还是来源于UserAgent样式表。这里需要特别提下UserAgent样式,它是浏览器提供的一组默认样式,如果你不提供任何样式,默认使用的就是UserAgent样式。
  • 最后,可以通过区域2和区域3来查看样式继承的具体过程。

以上就是CSS继承的一些特性,样式计算过程中,会根据DOM节点的继承关系来合理计算节点样式。
样式计算过程中的第二个规则是样式层叠。层叠是CSS的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在CSS处于核心地位,CSS的全称“层叠样式表”正是强调了这一点。关于层叠的具体规则这里就不做过多介绍了,网上资料也非常多,你可以自行搜索学习
总之,样式计算阶段的目的是为了计算出DOM节点中每个元素的具体样式,在计算过程中需要遵守CSS的继承和层叠两个规则。这个阶段最终输出的内容是每个DOM节点的样式,并被保存在ComputedStyle的结构内。

7. 构建Render Tree(渲染树)的流程:

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

16f040b792504d43~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.webp
最终产出一个Render Tree,其中包含屏幕上所有可见内容的内容和样式信息。
浏览器已经计算了哪些节点应该可见以及它们的样式,但是还没有计算它们在设备视口中的确切位置和大小,这是layout阶段该做的事,也称为“重排”
Render Tree中储存节点渲染信息的对象叫做Render Object(这个概念需要留意,下面会用到)

8. Layout(重排reflow),构建布局树

我们已经知道了DOM节点的大小,但是还不知道它在页面上的具体位置,这一步就是构建布局树,也叫重排。
主线程遍历Render Tree,并创建布局树,该树具有诸如xy坐标和边界框大小之类的信息。布局树的结构可能与DOM树类似,但它仅包含与页面上可见内容有关的信息。如果应用display: none,则该元素不属于布局树(但是,具有visibility: hidden的元素在布局树中)。同样,如果应用了具有类似的伪类p::before{content:“Hi!”},则即使它不在DOM中,它也将包含在布局树中。
但是现在有个问题,我们还不知道以什么顺序绘制它们,即不知道谁应该覆盖谁。
16f0e529b9648654~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.webp
其实很多资料中都会把上面构建渲染树的步骤放到构建布局树的步骤中

9. 分层、合成层(或者叫update layer tree)

如果我们是首次渲染,那就是分层,如果是更新操作,叫update layer tree。

9.1 分层

现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?
答案依然是否定的。
因为页面中有很多复杂的效果,如一些复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。如果你熟悉PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。
要想直观地理解什么是图层,你可以打开Chrome的“开发者工具”,选择“Layers”标签,就可以可视化页面的分层情况,如下图所示
image.png
从上图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面,你可以参考下图

现在你知道了浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。下面我们再来看看这些图层和布局树节点之间的关系,如文中图所示:

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。如上图中的span标签没有专属图层,那么它们就从属于它们的父节点图层。但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。
那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
第一点,拥有层叠上下文属性的元素会被提升为单独的一层。
页面是个二维平面,但是层叠上下文能够让HTML元素具有三维概念,这些HTML元素按照自身属性的优先级分布在垂直于这个二维平面的z轴上。你可以结合下图来直观感受下:

从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等,都拥有层叠上下文属性。
第二点,需要剪裁(clip)的地方也会被创建为图层。
不过首先你需要了解什么是剪裁,结合下面的HTML代码:

<style>div {width: 200;height: 200;overflow:auto;background: gray;} 
</style>
<body><div ><p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p><p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p><p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p> </div>
</body>

在这里我们把div的大小限定为200 * 200像素,而div里面的文字内容比较多,文字所显示的区域肯定会超出200 * 200的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在div区域,下图是运行时的执行结果

出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。你可以参考下图:

所以说,元素有了层叠上下文的属性或者需要被剪裁,满足这任意一点,就会被提升成为单独一层。

9.2 update layer tree

这一步实际是更新Render Layer的层叠排序关系。

9.3 补充解释:Render Object、Render Layer、Graphics Layer(又称Compositing Layer)和Graphics Context

a14a6f315f394b0844f7a1ee894593dd~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.webp

Render Object

首先我们有DOM树,但是DOM树里面的DOM是供给JS/HTML/CSS用的,并不能直接拿过来在页面或者位图里绘制。因此浏览器内部实现了Render Object
每个Render Object和DOM节点一一对应。Render Object上实现了将其对应的DOM节点绘制进位图的方法,负责绘制这个DOM节点的可见内容如背景、边框、文字内容等等。同时Render Object也是存放在一个树形结构中的。
既然实现了绘制每个DOM节点的方法,那是不是可以开辟一段位图空间,然后DFS遍历这个新的Render Object树然后执行每个Render Object的绘制方法就可以将DOM绘制进位图了?就像“盖章”一样,把每个Render Object的内容一个个的盖到纸上(类比于此时的位图)是不是就完成了绘制。
不,浏览器还有个层叠上下文的东西。这使得文档流中位置靠前位置的元素有可能覆盖靠后的元素。上述DFS过程只能无脑让文档流靠后的元素覆盖前面元素。
因此,有了Render Layer。

Render Layer

当然Render Layer的出现并不是简单因为层叠上下文等,比如opacity小于1、比如存在mask等等需要先绘制好内容再对绘制出来的内容做一些统一处理的css效果。
总之就是有层叠、半透明等等情况的元素就会从Render Object提升为Render Layer。不提升为Render Layer的Render Object从属于其父级元素中最近的那个Render Layer。当然根元素HTML自己要提升为Render Layer。
因此现在Render Object树就变成了Render Layer树,每个Render Layer又包含了属于自己layer的Render Object。
现在浏览器渲染引擎遍历 Layer 树,访问每一个 RenderLayer,然后递归遍历negZOrderList里的layer、自己的RenderObject、再递归遍历posZOrderList里的layer。就可以将一颗 Layer树绘制出来。
Layer 树决定了网页绘制的层次顺序,而从属于 RenderLayer 的 RenderObject 决定了这个 Layer 的内容,所有的 RenderLayer 和 RenderObject 一起就决定了网页在屏幕上最终呈现出来的内容。
层叠上下文、半透明、mask等等问题通过Render Layer解决了。那么现在:
开辟一个位图空间->不断的绘制Render Layer、覆盖掉较低的Layer->拿给GPU显示出来 是不是就完全ok了?
不。还有GraphicsLayers和Graphics Context

Graphics Layer(又称Compositing Layer)和Graphics Context

合成层的东西。
上面的过程可以搞定绘制过程。但是浏览器里面经常有动画、video、canvas、3d的css等东西。这意味着页面在有这些元素时,页面显示会经常变动,也就意味着位图会经常变动。每秒60帧的动效里,每次变动都重绘整个位图是很恐怖的性能开销。
因此浏览器为了优化这一过程。引出了Graphics Layers和Graphics Context,前者就是我们常说的合成层(Compositing Layer)
某些具有CSS3的3D transform的元素、在opacity、transform属性上具有动画的元素、硬件加速的canvas和video等等,这些元素在上一步会提升为Render Layer,而现在他们会提升为合成层Graphics Layer。每个Render Layer都属于他祖先中最近的那个Graphics Layer。当然根元素HTML自己要提升为Graphics Layer。
Render Layer提升为Graphics Layer的情况:

  • 3D 或透视变换(perspective、transform) CSS 属性
  • 使用加速视频解码的 元素
  • 拥有 3D (WebGL) 上下文或加速的 2D 上下文的 元素
  • 混合插件(如 Flash)
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
  • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

3D transform、will-change设置为 opacity、transform等 以及 包含opacity、transform的CSS过渡和动画 这3个经常遇到的提升合成层的情况请重点记住。
所以在元素存在transform、opacity等属性的css animation或者css transition时,动画处理会很高效,这些属性在动画中不需要重绘,只需要重新合成即可。
在前端页面,尤其是在动画过程中,由于 Overlap 重叠导致的合成层提升很容易发生。如果每次都将重叠的顶部 RenderLayer 提升为合成层,那将消耗大量的 CPU 和内存(Webkit 需要给每个合成层分配一个后端存储)。为了避免 “层爆炸” 的发生,浏览器会进行层压缩(Layer Squashing):如果多个 RenderLayer 和同一个合成层重叠时,这些 RenderLayer 会被压缩至同一个合成层中,也就是位于同一个合成层。但是对于某些特殊情况,浏览器并不能进行层压缩,就会造成创建大量的合成层。

RenderObject、 RenderLayer、 GraphicsLayer 是 Webkit 中渲染的基础,其中 RenderLayer 决定了渲染的层级顺序,RenderObject 中存储了每个节点渲染所需要的信息,GraphicsLayer 则使用 GPU 的能力来加速页面的渲染。

使用 合成层提升 减少重绘重排

提升为合成层干什么呢?普通的渲染层普通地渲染,用普通的顺序普通地合成不好吗?非要搞啥特殊待遇!
浏览器就说了:我这也是为了大家共同进步(提升速度)!看那些搞特殊待遇的,都是一些拖我们队伍后腿的(性能开销大),分开处理,才能保证整个队伍稳定快速的进步!
特殊待遇:合成层的位图,会交由 GPU 合成,比 CPU 处理要快。当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层。

  • 对布局属性进行动画,浏览器需要为每一帧进行重绘并上传到 GPU 中
  • 对合成属性进行动画,浏览器会为元素创建一个独立的复合层,当元素内容没有发生改变,该层就不会被重绘,浏览器会通过重新复合来创建动画帧

通过生成独立的Compositing Layer,让此层内的重绘重排不引起整个页面的重绘重排
在介绍渲染树的时候提到满足某些条件的 RenderObjectLayer 会被提升为合成层,合成层的绘制是在 GPU 中进行的,比 CPU 的性能更好;如果该合成层需要 Paint,不会影响其他的合成层;一些合成层的动画,不会触发 Layout 和 Paint。
下面介绍几种在开发中常用的合成层提升的方式:

使用transform和opacity书写动画

上文提出,如果一个元素使用了 CSS 透明效果的动画或者 CSS 变换的动画,那么它会被提升为合成层。并且这些动画变换实际上是应用在合成层本身上。这些动画的执行过程不需要主线程的参与,在纹理合成前,使用 3D API 对合成层进行变形即可。

#cube {transform: translateX(0);transition: transform 3s linear;
}#cube.move {transform: translateX(100px);
}
<body><div id="button">点击移动</div><div id="cube"></div><script>const btn = document.getElementById('button');btn.addEventListener('click', () => {const cube = document.getElementById('cube');cube.classList = 'move';});</script>
</body>

对于上面的动画,只有在动画开始后,才会进行合成层的提升,动画结束后合成层提升也会消失。这也就避免了浏览器创建大量的合成层造成的 CPU 性能损耗。
在这里插入图片描述

will-change

这个属性告诉了浏览器,接下来会对某些元素进行一些特殊变换。当 will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left、bottom、right 等需要设置明确的定位属性,如 relative 等),浏览器会将此元素进行合成层提升。在书写过程中,需要避免以下的写法:

*{ will-change: transform, opacity; }

这样,所有的元素都会被提升为单独的合成层,造成大量的内存占用。所以需要只针对动画元素设定 will-change,且动画完成之后,需要手动将此属性移除。

Canvas

使用具有加速的 2D Context 或者 3D Contex 的 Canvas 来完成动画。由于具有独立的合成层,Canvas 的改变不会影响其他合成层的绘制,这种情况对于大型复杂动画(比如 HTML5 游戏)更为适用。此外,也可以设置多个 Canvas 元素,通过合理的Canvas 分层来减少绘制开销。

paint(图层绘制),重绘

重绘是以合成层为单位的
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?
试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?
通常,你会把你的绘制操作分解为三步

  • 制蓝色背景;
  • 在中间绘制一个红色的圆;
  • 再在圆上绘制绿色三角形

渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:

从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。

其实Paint有两步,第一步是记录要执行哪些绘画调用,第二步才是执行这些绘画调用。第一步只是把所需要进行的操作记录序列化进一个叫做SkPicture的数据结构里,就是上面所说的待绘制列表。
接下来的第二步里会将待绘制列表中的操作replay出来,这里才是将这些操作真正执行:光栅化和填充进位图。主线程中和我们在Timeline中看到的这个Paint其实是Paint的第一步操作。第二步是后续的Rasterize步骤(见后文),其实在Rasterize之前会先分成图块,关于这两个概念的解释在最开始有提到。

主线程:生成待绘制列表,交给合成线程
合成线程:分成图块,交给栅格化线程
栅格化线程:栅格化(生成位图)
接着就是将栅格化的结果交给 GPU进程进行draw到浏览器上
这里其实有争议,栅格化的结果是直接由栅格化线程交给GPU,还是栅格化线程先将结果交给合成线程,合成线程再把结果交给GPU进程。

分成图块 + 栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:

如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?
那我们得先来看看什么是视口,你可以参看下图:

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是256x256或者512x512,如下图所示:

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:
image.png
通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。
相信你还记得,GPU操作是运行在GPU进程中,如果栅格化操作使用了GPU,那么最终生成位图的操作是在GPU中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:

从图中可以看出,渲染进程把生成图块的指令发送给GPU,然后在GPU中执行生成图块的位图,并保存在GPU的内存中。

draw

GPU进程把结果draw到浏览器上

引用

https://developer.chrome.com/blog/inside-browser-part3/
浏览器渲染详细过程:重绘、重排和 composite 只是冰山一角 - 掘金
https://segmentfault.com/a/1190000041295744
https://gist.github.com/paulirish/5d52fb081b3570c81e3a
渲染流程(下):HTML、CSS和JavaScript是如何变成页面的 | 浏览器工作原理与实践
如何不择手段提升scroll事件的性能
https://github.com/aooy/blog/issues/5

相关文章:

真正理解浏览器渲染更新流程

浏览器渲染更新过程 文章目录 浏览器渲染更新过程帧维度解释帧渲染过程一些名词解释Renderer进程GPU进程rendering(渲染) vs painting(绘制)⭐位图纹理Rasterize(光栅化) 1. 浏览器的某一帧开始&#xff1a;vsync2. Input event handlers3. requestAnimationFrame4. 强制重排(可…...

市场调研的步骤与技巧:助你了解市场需求

在当今快速发展的市场中&#xff0c;进行有效的市场研究对于了解消费者的行为、偏好和趋势至关重要。适当的市场研究可以帮助公司获得对目标受众的有价值的见解&#xff0c;创造更好的产品和服务&#xff0c;并提高客户满意度。今天&#xff0c;小编和大家一起讨论一下怎么做市…...

ansible的个人笔记使用记录-个人心得总结

1.shell模块使用&#xff0c;shell模块------执行命令&#xff0c;支持特殊符 ansible all -m shell -a yum -y install nginx ansible all -m shell -a systemctl restart nginx ansible all -m shell -a systemctl stop nginx && yum -y remove nginx2. file模块…...

相机数据恢复!详细步骤解析(2023新版)

和朋友在外面旅游用相机拍了好多有意义的照片和视频&#xff0c;但是导入电脑后不知道是被我删除了还是什么原因&#xff0c;这些照片都不见了&#xff0c;请问有方法恢复吗&#xff1f;” 在数字摄影时代&#xff0c;我们依赖相机记录珍贵的瞬间。然而&#xff0c;相机数据丢失…...

LNK2001: unresolved external symbol __imp___std_init_once_begin_initialize 问题解决

LNK2001: unresolved external symbol __imp___std_init_once_begin_initialize 解决 文章目录 问题背景方法一&#xff1a;使用预编译指令方法二&#xff1a;使用相同的环境 参考链接附录 问题背景 Visual Studio 2019 对 CMakeLists.txt 的支持不是很好&#xff0c;使用 “文…...

修改switch Nand无线区码 以支持高频5G 信道

环境&#xff1a;NS switch 问题&#xff1a;日版&#xff0c;港版无法连接大于44信道的5G WIFI 解决办法&#xff1a;修改PRODINFO.dec的WIFI 区域码 背景&#xff1a;我的switch是最早买的港版的一批&#xff0c;WIFI 只能连接日本的信道&#xff0c;家里的路由器是国行的&am…...

基于SpringBoot的课程答疑系统

目录 前言 一、技术栈 二、系统功能介绍 学生信息管理 科目类型管理 老师回答管理 我的收藏管理 学生问题 留言反馈 交流区 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息互联网信息的飞速发展&#xff0c;无纸化作业变成了一种趋势&#x…...

JAVA中的泛型

一、泛型的概念 泛型是JAVA中的一个重要的概念&#xff0c;它允许你在编译时指定数据类型&#xff0c;从而使得代码更加灵活&#xff0c;更加通用。通过泛型&#xff0c;你可以在通用代码上操作不同数据类型&#xff0c;使得代码更加具有通用性。 二、泛型的使用场景 1、泛型…...

日撸代码300行:第73天(固定激活函数的BP神经网络,训练与测试过程理解)

进一步梳理理解了一下正向和反向传播。Forward 是利用当前网络对一条数据进行预测的过程&#xff0c;BackPropagation 是根据误差进行网络权重调节的过程。 完整的代码在72天&#xff0c;这里只粘贴Forward和BackPropagation两个方法。 /*** *********************************…...

css中常用单位辨析

辨析 px&#xff1a;像素&#xff1b;css中最普遍最常用的单位&#xff0c;不管在何种设备或分辨率上&#xff0c;1px始终代表屏幕上的一个像素。 %&#xff1a;百分比&#xff1b;基于父元素相对属性的百分比。 em&#xff1a;当前字体大小的倍数&#xff1b;基于父元素字体…...

Unity 一些常用特性收集

常用的类的特性 特性效果[Serializable]可序列化&#xff0c;作为一个子属性显示在Inspector面板[RequireComponent(typeof(CoomponnetName))]该类挂载的游戏物体&#xff0c;需要要有对应的组件[DisallowMultipleComponent]不允许挂载多个该类或其子类[ExecuteInEditMode]允许…...

select实现服务器并发

select的TCP服务器代码 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/select.h> #include…...

【Spring底层原理】BeanFactory的实现

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 容器实现 一、BeanFactory实现的特点1.1 Be…...

c++---I/o操作

5、文件操作 程序运行时产生的数据都属于临时数据&#xff0c;程序一旦运行结束都会被释放。 我们可以通过文件将数据持久化 C中对文件操作需要包含头文件 <fstream> 文件类型分为两种&#xff1a; 文本文件 - 文件以文本的ASCII码形式存储在计算机中二进制文件 - 文…...

UG\NX二次开发 用程序修改“用户默认设置”

文章作者:里海 来源网站:《里海NX二次开发3000例专栏》 简介 可以用程序修改“用户默认设置”吗?下面是用代码修改“用户默认设置->基本环境->用户界面->操作记录->操作记录语言”的例子。 效果 代码 #include <uf_defs.h> #include <NXOpen/NXExcept…...

什么是信号处理?如何处理信号?

C语言信号处理详解 第一部分&#xff1a;什么是信号&#xff1f; 信号是一种进程间通信的机制&#xff0c;用于通知进程发生了某种事件或异常情况。在C语言中&#xff0c;信号是一种软件中断&#xff0c;它可以被操作系统或其他进程发送给目标进程。每个信号都有一个唯一的数…...

谈谈 Redis 数据类型底层的数据结构?

谈谈 Redis 数据类型底层的数据结构? RedisObject 在 Redis 中&#xff0c;redisObject 是一个非常重要的数据结构&#xff0c;它用于保存字符串、列表、集合、哈希表和有序集合等类型的值。以下是关于 redisObject 结构体的定义&#xff1a; typedef struct redisObject {…...

九、GC收集日志

JVM由浅入深系列一、关于Java性能的误解二、Java性能概述三、了解JVM概述四、探索JVM架构五、垃圾收集基础六、HotSpot中的垃圾收集七、垃圾收集中级八、垃圾收集高级👋GC收集日志 ⚽️1. 认识GC收集日志 垃圾收集日志是一个重要的信息来源,对于与性能相关的一些悬而未决的…...

SimpleCG动画示例--汉诺塔动画演示

前言 SimpleCG的使用方法在前面已经介绍了许多&#xff0c;有兴趣的同学如果有去动手&#xff0c;制作一些简单动画应该没多大问题的。所以这次我们来演示一下简单动画。我们刚学习C语言的递归函数时&#xff0c;有一个经典例子相信很多同学都写过&#xff0c;那就是汉诺塔。那…...

反弹shell脚本(php-reverse-shell)

平时经常打靶机 这里贴一个 反弹shell的脚本 <?php // php-reverse-shell - A Reverse Shell implementation in PHP // Copyright (C) 2007 pentestmonkeypentestmonkey.net // // This tool may be used for legal purposes only. Users take full responsibility // f…...

XSS-labs

XSS常见的触发标签_xss标签_H3rmesk1t的博客-CSDN博客 该补习补习xss漏洞了 漏洞原理 网站存在 静态 和 动态 网站 xss 针对的网站 就是 动态网站 动态网站会根据 用户的环境 与 需求 反馈出 不同的响应静态页面 代码写死了 只会存在代码中有的内容 通过动态网站 用户体…...

C++简单实现AVL树

目录 一、AVL树的概念 二、AVL树的性质 三、AVL树节点的定义 四、AVL树的插入 4.1 parent的平衡因子为0 4.2 parent的平衡因子为1或-1 4.3 parent的平衡因子为2或-2 4.3.1 左单旋 4.3.2 右单旋 4.3.3 先左单旋再右单旋 4.3.4 先右单旋再左单旋 4.4 插入节点完整代码…...

UE4 Cesium 与ultra dynamic sky插件天气融合

晴天&#xff1a; 雨天&#xff1a; 雨天湿度&#xff1a; 小雪&#xff1a; 中雪&#xff1a; 找到该路径这个材质&#xff1a; 双击点开&#xff1a; 将Wet_Weather_Effects与Snow_Weather_Effects复制下来&#xff0c;包括参数节点 找到该路径这个材质&#xff0c;双击点开&…...

SpringCloud Gateway--Predicate/断言(详细介绍)下

&#x1f600;前言 本篇博文是关于SpringCloud Gateway–Predicate/断言&#xff08;详细介绍&#xff09;下&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以…...

SOC芯片学习--GPIO简介

原创 硬件设计技术 硬件设计技术 2023-07-20 00:04 发表于广东 收录于合集#集成电路--IC7个 一、GPIO定义、分类&#xff1a; GPIO&#xff08;英语&#xff1a;General-purpose input/output&#xff09;&#xff0c;通用型之输入输出的简称&#xff0c;其接脚可以供使用者由…...

skywalking源码本地编译运行经验总结

前言 最近工作原因在弄skywalking&#xff0c;为了进一步熟悉拉了代码下来准备debug&#xff0c;但是编译启动项目我就费了老大劲了&#xff0c;所以准备写这篇&#xff0c;帮兄弟们少踩点坑。 正确步骤 既然是用开源的东西&#xff0c;那么最好就是按照人家的方式使用&…...

K8s架构简述

以部署一个nginx服务说明kubernetes系统各个组件调用关系&#xff1a; 一旦kubernetes环境启动之后&#xff0c;master和node都会将自身的信息存储到etcd数据库中 一个nginx服务的安装请求会首先被发送到master节点的apiServer组件 apiServer组件会调用scheduler组件来决定到底…...

linkedlist和arraylist的区别

LinkedList和ArrayList都是常见的数据结构&#xff0c;用于存储和操作集合元素&#xff0c;如果需要频繁进行插入和删除操作&#xff0c;LinkedList可能更适合。如果需要快速随机访问和较小的内存占用&#xff0c;ArrayList可能更合适。 以下是它们之间存在一些关键的区别&…...

[尚硅谷React笔记]——第2章 React面向组件编程

目录&#xff1a; 基本理解和使用&#xff1a; 使用React开发者工具调试函数式组件复习类的基本知识类式组件组件三大核心属性1: state 复习类中方法this指向&#xff1a; 复习bind函数&#xff1a;解决changeWeather中this指向问题&#xff1a;一般写法&#xff1a;state.htm…...

嵌入式学习笔记(40)看门狗定时器

7.5.1什么是看门狗、有何用 (1)看门狗定时器和普通定时器并无本质区别。定时器可以设定一个时间&#xff0c;在这个时间完成之前定时器不断计时&#xff0c;时间到的时候定时器会复位CPU&#xff08;重启系统&#xff09;。 (2)系统正常工作的时候当然不希望被重启&#xff0…...

手机网站怎么建设/seo推广专员招聘

如下例程ReflectTester类进一步演示了Reflection API的基本使用方法。 ReflectTester类有一个copy(Object object)方法&#xff0c;这个方法能够创建一个和参数object同样类型的对象&#xff0c;然后把object对象中的所有属性拷贝到新建的对象中&#xff0c;并将它返回。 这个例…...

山东网站建设优化技术/无锡seo培训

今天同事给了两个SQL&#xff0c;超级大&#xff0c;一个表8000多万&#xff0c;一个表7800万&#xff0c;原语句如下&#xff1a;[more]update CHANNEL_CHENGDU.o_user_CONS partition(p201011) ASET unuser_flag (SELECT unuser_flagFROM CHANNEL_CHENGDU.o_user partition(…...

网站版块设计/企业推广方式有哪些

背景下面的脚本&#xff0c;在Linux上运行良好&#xff0c;在SUNOS执行的时候报语法错误。#! /bin/sh#支持fwu的使用fwu 不支持的使用fuPS_TYPE"ps -fwu"do_psps -fwu 2>/dev/nullif [ "$?" -eq 1 ]thenPS_TYPE"ps -fu"fiOSTYPEuname -a | a…...

做类似淘宝的网站需多少资金/营口建网站的公司

突然不能调试 JavaScript 脚本了 通过 C:\Program Files\Common Files\Microsoft Shared\VS7Debug\vs7jit.exe /regserver 设定了&#xff0c;记下来 转载于:https://www.cnblogs.com/icehong/archive/2004/09/29/47732.html...

做网站去哪里找/win7优化极致性能

Apple Pay交通卡功能在国内又下一城。继天津之后&#xff0c;Apple Pay现在正式实现对长沙潇湘卡的支持。这意味着&#xff0c;用户可以使用Apple Pay在长沙乘坐公交、地铁和磁悬浮。手持iPhone 或Apple Watch的用户可以按照以下的方式进行开通&#xff1a;iPhone用户直接打开钱…...

WordPress软件连接不了网站/怎样在平台上发布信息推广

算法简介 SPFA(Shortest Path Faster Algorithm)是Bellman-Ford算法的一种队列实现&#xff0c;减少了不必要的冗余计算。也有人说SPFA本来就是Bellman-Ford算法&#xff0c;现在广为流传的Bellman-Ford算法实际上是山寨版。 求单源最短路的SPFA算法的全称是&#xff1a;Short…...