浏览器的渲染过程2.0 — Composite
前言
上一篇文章: 《浏览器的渲染机制》里,大概介绍了浏览器从下载完第一份document文件,到渲染出整个页面所发生的事情(也称作:关键路径渲染
),其中包括:1、构建对象模型(DOM,CSSOM),2、构建渲染树(RenderTree),3、布局,4、渲染。
这一篇文章我们将参考chrome浏览器的内核WebKit, 详细介绍下浏览器呈现引擎的一些概念和渲染中的一个关键环节:Composite(渲染层合并),它所做的事情和带来的效果。
浏览器的主要结构
市面上主流的几款浏览器,基本上都是由下面几个结构组成:
- 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
- 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
- 呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
- 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
- JavaScript 解释器。用于解析和执行 JavaScript 代码。
- 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
其中值得注意的是,与其他浏览器不同的是:chrome浏览器中每个tab页对应一个呈现引擎实例,每个tab页都是一个单独的进程。
呈现引擎
webkit的呈现引擎,官方提供的一张主流程图是:
其中有些名词和 《浏览器的渲染机制》里提到的不一样,比如:Style Rules,这是webkit自己引入的几个概念,这里先解释一下,方便大家理解:
- DOM Tree:浏览器将HTML解析成树形的数据结构。
- Style Rules:浏览器将CSS解析成树形的数据结构,对应我们的CSSOM。
- Render Tree:DOM和CSSOM合并后生成Render Tree。
- layout: 布局。有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从 属关系,从而去计算出每个节点在屏幕中的位置。
- painting: 渲染。按照算出来的规则,通过显卡,把内容画到屏幕上。
- reflow(回流/重排):当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染,这个回退的过程叫 reflow。
Dirty 位系统
为了避免所有细小的更改都要从根节点进行整体布局,浏览器采用了一种“dirty 位”系统。如果某个RenderTree上的Node发生了更改,便将自身标注为“dirty”,表示需要进行布局。
有两种标记:“dirty”和“children are dirty”。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代需要布局
reflow时,浏览器会对Render Tree进行遍历,而仅仅只对标注了“dirty “的Node进行布局。
- repaint(重绘):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
上面的过程可以合并成:
实际场景下,渲染流程可以分为下面三种,layout和point这个过程是可以避免的:
- 正常情况:JS/CSS -> 计算样式 -> 布局 -> 绘制 -> 渲染层合并
- 不需要重新布局(仅触发重绘):JS/CSS -> 布局 -> 绘制 -> 渲染层合并
- 既不需要重新布局,也不需要重新绘制(不触发回流):JS/CSS -> 布局 -> 渲染层合并
显而易见,第三种渲染流程浏览器的处理是最快的,那什么情况下的渲染是第三种情况呢,这里就不得不提到Compsite了
Compsite
上一篇我们提到,浏览器渲染时,对渲染树有着一个RenderObject -> RenderLayout的转换过程:
实际上,Chrome拥有两套不同的渲染路径:硬件加速路径和旧软件路径。
硬件加速路径会将一些图层的合成交给GPU处理,比CPU处理更快,而我们的RenderLayout(有些地方又称作PaintLayers)并不能作为GPU的输入,这里会将RenderLayout再转换成GraphicsLayers:
某些特殊的RenderLayout会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。
而每个GraphicsLayer(合成层单独拥有的图层) 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后显示到屏幕上。
很绕口对不对= =,人性话来说,与RenderObject转换成RenderLayer一样,浏览器也是根据“某些规则”,选中一些特殊的RenderLayout节点(天选之子),这些节点将被称为Compositing Layers,Compositing Layers与其他的普通节点不一样的是他们拥有自己的GraphicsLayer,而那些没有被选中的节点,会和父层公用一个GraphicsLayer。
每个GraphicsLayer都拥有GraphicsContext,GraphicsContext输出的位图会作为纹理上传到GPU中。
什么是纹理?可以把它想象成一个从主存储器(例如 RAM)移动到图像存储器(例如 GPU 中的 VRAM)的位图图像(bitmapimage)
Chrome 使用纹理来从 GPU上获得大块的页面内容。通过将纹理应用到一个非常简单的矩形网格就能很容易匹配不同的位置(position)和变形(transformation)。这也就是3DCSS 的工作原理,它对于快速滚动也十分有效。
RenderLayer -> GraphicsLayer,合成层的创建标准
什么情况下能使元素获得自己的层?虽然 Chrome的启发式方法(heuristic)随着时间在不断发展进步,但是从目前来说,满足以下任意情况便会创建层:
- 3D 或透视变换(perspective transform) CSS 属性
- 使用加速视频解码的 元素 拥有 3D
- (WebGL) 上下文或加速的 2D 上下文的 元素
- 混合插件(如 Flash)
- 对自己的 opacity 做 CSS动画或使用一个动画变换的元素
- 拥有加速 CSS 过滤器的元素
- 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
- 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
合成层的性能优化:
对于提升为合成层有以下几个好处:
- 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于 transform 和 opacity 效果,不会触发 layout 和 paint (当当当,这里就是第三种渲染模式的场景)
注意:
- 提升到合成层后合成层的位图会交GPU处理,但请注意,仅仅只是合成的处理(把绘图上下文的位图输出进行组合)需要用到GPU,生成合成层的位图处理(绘图上下文的工作)是需要CPU。
- 当需要repaint的时候可以只repaint本身,不影响其他层,但是paint之前还有style, layout,那就意味着即使合成层只是repaint了自己,但style和layout本身就很占用时间。
- 仅仅是transform和opacity不会引发layout 和paint,那么其他的属性不确定。
总结来说,一般一个元素开启硬件加速后会变成合成层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能。
那么针对上面这些特点,可以做的性能优化有:
提升动画效果的元素
合成层的好处是不会影响到其他元素的绘制,因此,为了减少动画元素对其他元素的影响,从而减少paint,我们需要把动画效果中的元素提升为合成层。
提升合成层的最好方式是使用 CSS 的will-change
属性。从上一节合成层产生原因中,可以知道 will-change 设置为opacity、transform、top、left、bottom、right
可以将元素提升为合成层。对于不支持will-change的浏览器版本,可以加上transform属性,比如:transform: translateZ(0)
使用 transform 或者 opacity 来实现动画效果
上文提到transform和opaciry属性不会引发layout和paint,值得注意的是,只有元素提升到合成层后才不会引发,普通元素修改transform和opacity还是会引发的。
减少绘制区域
对于不需要重新绘制的区域应尽量避免绘制。
比如一个 fix 在页面顶部的固定不变的导航header,当页面内容某个区域 repaint 时,在没有提升为合成层时,整个屏幕包括 fix 的 header 也会被重绘。而对于固定不变的区域,我们期望其并不会被重绘,因此可以通过之前的方法,将其提升为独立的合成层。
如何查看合成层
- Chrome DevTools -> Rendering -> Layer borders
然后页面的合成层会用黄色的边框框出来
- Chrome DevTools -> Layers
左边会详细列出页面的所有合成层,点击时会将这个合成层在页面的具体位置显示出来。下面的Details和Profiler会显示这个合成层的详细信息(大小,加载时间等...)
合成层可能会遇到的问题
由于合成层本身是占用内存的,当浏览器出现大量无法被压缩的合成层时(层爆炸),会导致内存紧张,GPU资源过度消耗等问题,具体可参考CSS3硬件加速也有坑!!!,这篇文章写的。
参考资料
https://fed.taobao.org/blog/2016/04/26/performance-composite/
https://www.html5rocks.com/zh/tutorials/speed/layers/
https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/#Global_and_Incremental
https://segmentfault.com/a/1190000014520786
https://div.io/topic/1348
https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count