近年来,随着各种virtual dom的技术大行其道,越来越多新入行的同学对于dom的概念,乃至浏览器渲染的知识的了解和熟悉程度都越来越低了,当然,毕竟浏览器渲染牵涉到内容广大而深远,不同浏览器的策略又不完全相同,但是近来团队里的童鞋频繁遇到了动效实现的具体方式上选择的困扰,于是笔者从前到后梳理了一下有关的知识点,只是没想到不理不知道,一理吓一跳,整个过程感觉自己也有了很多新的收获,真可谓温故而知新。
首先是大家都熟悉的一张传统的浏览器渲染流程图,图来源为耗子叔的blog,笔者最开始入门的时候也是看的这张图,原po中还有一个渲染过程的视频,非常直观,简单的说:
1、浏览器从上往下(HTML 文档的顺序)逐行解析整个网页:(当然,这里还有同/异步下载远程资源的过程,不过这里不是本文的重点,就略过了)
1)对于类html文件,会转换生成为Dom Tree(DOM的概念);
2)对于css等样式文件,解析css并转换为CSSOM;
3)对于script文件,按照其行为遵循以上操作;
2、紧接着根据dom tree和拿到的css匹配构造并生成render tree;
3、进行layout(因为实际过程中会产生多次layout,而国内外对这个过程的命名和不同厂商对这个过程的命名都有不同,常用的英文名还有:reflow layouting relayout,中文名也有“布局”、“回流”、“重排”等多种称呼,本文中笔者统一使用chrome中使用的方式)
4、进行paint(同样,该过程也会发生多次,也有不同的称呼:如repaint redraw,中文一般称作“重绘”)
5、进行composite layers
对于第5条,也许你并未在上图中看到,让我们随意打开一个网站,打开调试工具具体的看看一个网页加载过程:
在chrome中,可以看到,
在paint后面往往很偷偷摸摸的跟着一个小条,名为“Composite layers”,这也算是现代浏览器对渲染的进一步优化吧,所以现代浏览器的渲染可以简化为以下的几个步骤:
不过上图并没有记录dom tree和render tree的生成过程,相当于整个construct过程都省略了,而是着重强调了开发者可控制的剩余几个过程,接下来我们就来谈一下在这几个过程中,浏览器都做了什么:
1、JavaScript 和 style:这两个阶段就相当于我们的代码动态的修改了dom元素和css rules,然后需要重新进行一次浏览器渲染操作;
2、layout:顾名思义就是进行布局操作,确定每个的HTMLElement的几何位置关系,确定自身的大小等(可以理解为几乎所有的几何位置关系都会在此过程中进行,有关页面结果的所有操作都在此过程中进行);
3、paint: 同样顾名思义为渲染操作,即为每个确定位置的HTMLElement填充颜色、图像、文字、边框、阴影等;
4、composite:合成图层(这个阶段又通常被称作“复合层” 、“合成层”、 “组合层”等);
看到这里可能又会奇怪了,不是只有布局,渲染么?为什么还需要合成呢?其实早在layout阶段,当dom直接能够确定了位置关系之后,一种新的关系就生成了:遮挡关系。因为dom并不是都像现在的flex布局那样所有项目之间都是不会存在遮挡关系的,既然产生了遮挡关系,所以分层就应运而生了,随之而产生的还有我们熟悉的层叠上下文(TSC),定义并规范了这种情形,在只要满足它的条件,浏览器就会产生一个新的图层用于存放TSC,然后等到了最后的composite阶段,进行层压缩(Layer Squashing)避免过多的层产生过多的开销,影响页面性能。
好了,说到页面性能我们终于到了我们的正题了。虽然第一个阶段是我们修改页面时的必要开销,但是剩下的三个阶段,都相当于是浏览器的应激反应,实际上并不是我们所期望的,接下来让我们来再仔细的看看这三个阶段产生什么具体的影响:
1、layout:
如前所述,在布局阶段,所有dom元素的位置、大小等几何关系都会确定,而反过来说,所有有关几何关系的操作都会不可避免的触发这个阶段,而这个阶段对于浏览器渲染而言,不得不说是一个非常昂贵的开销,且撇开它本身的开销不谈,由于页面的layout,不可避免的还会触发paint和composite,所以这个阶段带来的将是最昂贵的性能开销。
那么什么样的操作会触发layout呢?
简单的来讲有以下几个原因:
1) dom元素的几何属性变化,以及dom的显示和隐藏(单指display,不包括visibility)
2) DOM tree的结构变化
3) 获取某些属性
4) window的resize或者scroll
5) font-size的改变
而从另外个角度,从触发源来说,又有两种情形会触发layout:
1) 一种是写重排,即每次尝试给这些值赋值会引起layout:width height left top margin padding
2) 另一种是读重排,即每次尝试读取这些值的时候就会引起layout:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、 clientTop、clientLeft、clientWidth、clientHeight、(getComputedStyle() or currentStyle in IE),尤其值得一提的是,浏览器在实际处理layout的时候会做一些优化,来节省性能开销,它会选择将一些操作放入队列中然后批量layout,但是当遇到读重排的情况的时候,这个逻辑则不会触发。
第一次发现还有读重排的情况的时候笔者也是震惊的,但是笔者再三核对之后,发现的确是这么一回事,而且还发现了一份完整的列表,不仅仅是包含对某些属性的访问,调用某些方法也会引起layout:
Element: clientHeight, clientLeft, clientTop, clientWidth, focus(), getBoundingClientRect(), getClientRects(), innerText, offsetHeight, offsetLeft, offsetParent, offsetTop, offsetWidth, outerText, scrollByLines(), scrollByPages(), scrollHeight, scrollIntoView(), scrollIntoViewIfNeeded(), scrollLeft, scrollTop, scrollWidth
Frame, Image: height, width
Range: getBoundingClientRect(), getClientRects()
SVGLocatable: computeCTM(), getBBox()
SVGTextContent: getCharNumAtPosition(), getComputedTextLength(), getEndPositionOfChar(), getExtentOfChar(), getNumberOfChars(), getRotationOfChar(), getStartPositionOfChar(), getSubStringLength(), selectSubString()
SVGUse: instanceRoot
window: getComputedStyle(), scrollBy(), scrollTo(), scrollX, scrollY, webkitConvertPointFromNodeToPage(), webkitConvertPointFromPageToNode()
2、paint
讲完了最重的layout我们来看看下一个paint,它是为填充layer的颜色、图像、文字、边框、阴影等而生的,即所有相关的改变都会触发该阶段,通常的如:opacity visibility outline transform等,值得一提的是,我们在通常修改dom的位置的时候之所以推荐使用transform而不使用left/top的原因就在于此,因为对left和top的修改是必定会触发layout的,而对transform的修改则只会触发paint,性能更好一些。
不过谈到paint就不得不提一下层叠上下文(the stacking context)了,上文简单了提及了一下TSC产生的背景,这里列一下它产生的条件:
1)根元素
2)有明确的定位属性(relative、fixed、sticky、absolute,其中相对和绝对定位的zIndex不能为auto)
3)opacity 小于 1
4)具备filter、mask、mix-blend-mode(不为normal)、reflection、column-count(不为auto)、column-width(不为auto)、-webkit-overlfow-scrolling(值为touch)、isolation(值为isolate)属性的元素
5)具备transform属性(不为none)
6)backface-visibility hidden的元素
7) 有对于opacity、transform、fliter、backdrop-filter应用animation或者transition的元素
8) z-index不为auto的flex项目
9) isolation 属性被设置为 "isolate"的元素
更详细的也可以参考MDN的层叠上下文说明
当然,paint阶段和layout阶段也有类似的规律,触发它虽然不会触发layout,但是仍然会不可避免的触发composite,所以性能开销依然不低,而且在实际工作中我们的页面中的动效实际是不可避免的,我们依赖paint阶段来实现也是一个通用方法。
有关layout与paint,优化这两个阶段的基本原则就是:
“减少不必要的dom操作”
1)尽量使用class和cssText来进行必要的style操作以减少layout
2)将复杂的修改离线化(display:none)
3) 不要频繁的使用computedStyles,将需要的值读一次缓存起来处理(offsetTop等)
3、composite
前文我们已经提过,在layout和paint阶段之后,几乎一定会经过composite阶段将之前的layer合并,而为了避免过多的合成层影响渲染效率,浏览器一般会进行层压缩(Layer Squashing)的操作来节省资源,而压缩操作会遇到无法压缩层(squashing Clipping Container Mismatch)的情况,进而可能导致层爆炸(Layer Explosion),这是我们需要特别注意的。
虽然有“层爆炸”这只黑天鹅需要我们处理,但是多数时候,只要我们能够控制合成层的产生,不要让它过量出现,这个问题就通常可以避免,那么合成层到底是怎么出现的呢?总的来说它还是和之前的遮挡关系以及我们常常使用的硬件加速有关:
1)硬件加速的iframe、flash等
2)video及覆盖在video标签上的控制栏
3)3D canvas
4)3d transform
5)backface-visibility为hidden的元素(这是css 3d的一部分,是指3d渲染元素的背部是否可见)http://h5.51ping.com/app/app-threejs-demo/cards.html
6)对 opacity、transform、fliter、backdrop-filter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,合成层也会失效)
7)will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)
8)元素有一个z-index较低且包含一个合成层的兄弟元素(即该元素在合成层上渲染)
当然以上这些还不是合成层出现的全部条件,有时候因为其他情况也会间接的导致合成层的出现:
1)后代有transform、opacity(<1)、mask、filter、reflection的属性
2)后代overflow不为visible,(特别的,如果本身定位明确,则需要满足zIndex不为auto的条件)
3)有合成层后代,且自身fixed定位或者具备perserves-3d属性或者具备perspective属性
4)以上都是由于后代为合成层,导致自身提升为合成层的情况,特别的,自身为合成层,兄弟节点的后代overflow不为visible,且具备定位的时候,会将兄弟节点提升为合成层
听到这里是不是已经晕得不行了?笔者当初看到这里的时候一度想起了以前在学校里背法典的情形。。
让我们换个角度来看待合成层,首先,打开一个页面:
然后切换到layers选项卡(笔者使用的chrome版本号是 58.0.3029.110,低于此版本的同学请在timeline->layers里查看,如果还没有最好就升下级吧),通过menu栏的操作按钮改变了页面的角度之后,我们就能比较直观的看到一个页面的合成层了(因为合成层是浏览器最终展示给用户的分成,所以能够有如此比较方便的观察手段,比之前的paint阶段的的layer则没这么方便了,不过那个阶段的分层规则也相对简单),左边是合成层对应dom,右边则是具体排列,你还可以勾选顶部的paints,那么它将会绘制出layer的内容,接着我们选中左边的一个具体的层,然后我们可以看到如下信息:
从上图可以看出,我们可以知道一个具体的合成层的原因、使用的内存、还有宽高等信息,关键就在于这个内存使用,虽然我们之前就知道通过开启硬件加速(其实就是生成合成层的一种方式,来提升体验),但是由于合成层带来了额外的内存开销,过渡使用又会再一次引起性能问题。(写到这里笔者脑海里突然响起了“均衡守护万物之基”的声音。。。)
看到这里你会不会觉得很奇怪,当初我们为了提升页面性能而打开的硬件加速,到了现在却变成了拖慢页面性能的潜在原因,说起来也讽刺,但是其实性能优化就是这样,很难会有一个放之四海而皆准而法则,往往需要我们在实际工作中不断改进和测试我们的页面,才是最正确的途径。
不过话说回来,composite阶段的优化和前两个阶段的优化确实有着不一样的困难,加之纷繁复杂的规则,我们在实际应对时往往会比较没有头绪,其实,握住硬件加速、遮挡是大部分成因,内存是重要缺陷的脉络去整理自己的html,也就不那么复杂了。
说完了合成层如何“被”产生我们再来说说如果手动产生合成层:
1、will-change: transform/opacity;
2、transform: tanslateZ(0)/translate3d(0,0,0);
虽然它有内存占用的缺陷,但是为了避免有些动效频繁地引起页面的paint和layout,有节制的使用合成层也能够提高我们的页面性能,这也是硬件加速盛行的原因,其他熟悉硬件加速的大家看到以上两个途径第一反应想必也是“这不是开启硬件加速吗?”其实,我们回顾下硬件加速的机制:
“通过开启硬件加速,将本来由CPU的计算量一部分分配给GPU,提高页面性能”
其实,合成层也是在基于这个机制在工作,诉求都是为了提高页面性能,只是由于本身浏览器并不会特别“智能”的响应我们的所有诉求,而且也由于其内部机制的限制,才会出现如“层爆炸”的黑天鹅。
限于篇幅限制,笔者便不再赘述有关“层爆炸”的相关知识点了,有兴趣可以参看淘宝前端团队的无线性能优化:Composite,里面对上文提到的很多规则都提供了完整demo。
另外如果你还需要更为深入的明确哪些dom操作会触发哪些过程,则可以参考csstriggers,里面有比较完整的说明。
更多参考文献:
浏览器的工作原理:http://taligarsiel.com/Projects/howbrowserswork1.htm
Composite in Chrome: http://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome
will-change: https://dev.opera.com/articles/css-will-change-property/
Layer Introduction: https://www.html5rocks.com/zh/tutorials/speed/layers/
How to trigger layout: http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html
What`s mean of repaint: http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/