本文以3DWeb动画为例,详细介绍了整个浏览器渲染页面的过程和原理。CSS33D行星运动演示页面请点击:Demo-CSS33D行星运动[1]。嗯,可能有人打不开demo或者页面乱了,贴几张效果图:(图片有点大,请耐心等待)再来一张随机截图的CSS33D行星运行效果图:强烈推荐那个你点击进入演示-CSS33D行星运动[2]页感受CSS33D的魅力,毕竟图片能展示的是有限的。那么这个CSS33D行星动画的制作过程就不详细说了。本文重点介绍Web动画和性能优化。详细的CSS33D可以参考之前的博客:【CSS3进阶】炫酷3D旋转透视[3]。简单思路:以上篇文章制作的3D照片墙为原型,进行改造;每个球体的制作我想了很多方法,最后采用了这种折中的方法。每个球体本身也是一个CSS33D图形。那么在制作过程中使用Sass写CSS可以减少很多繁琐的写CSS动画的过程;Demo中使用Javascript写了一个鼠标跟随的监听事件。如果去掉这个事件,整个行星运动动画本身就是纯CSS实现的。下面将进入本文的重点。我会从性能优化的角度讲浏览器渲染显示原理、浏览器重绘重排、动画性能检测与优化等:浏览器渲染显示原理及其对网页动画的影响。说的有点大,我们知道不同浏览器的内核(渲染引擎,RenderingEngine)是不一样的,比如最主流的chrome浏览器的内核是Blink内核(在Chrome(28及之后的版本)、Opera(15及更高版本)和Yandex)、适用于Firefox的Gecko和适用于IE的Trident。浏览器内核负责解释网页的语法并渲染(显示)网页,不同浏览器内核的工作原理并不完全相同。所以其实下面会主要讨论chrome浏览器下的渲染原理。因为chrome内核渲染的可考证资料很多,其他内核的浏览器我们不敢妄下定论,所以下面的讨论默认针对chrome浏览器。首先抛出一个小结论:用transform3dapi代替transformapi,强制开启GPU加速。这里我们说说GPU加速。为什么GPU可以加速3D变换?这一切都要从浏览器的底层渲染说起,浏览器渲染展示网页的过程是家常便饭,面试时必须问到。大致可以分为:解析HTML(HTMLParser)构建DOM树(DOMTree)构建渲染树(RenderTree)绘制渲染树(Painting)我找了一张很经典的图:浏览器渲染页面流程这个渲染流程用作一个基础知识,继续深入。当页面被加载和解析时,它在浏览器中代表了一个非常熟悉的结构:DOM(DocumentObjectModel,文档对象模型)。当浏览器呈现页面时,它会使用许多未向开发人员公开的中间表示,其中最重要的结构是层。该层是本文的重点:在Chrome中,有不同类型的层:RenderLayer(负责DOM子树)、GraphicsLayer(负责RenderLayer子树)。接下来我们要讨论的是GraphicsLayer层。GraphicsLayer图层作为纹理上传到GPU。这个纹理在这里很重要,那么,什么是纹理(texture)呢?这里的纹理指的是GPU的一个术语:你可以把它看成是从主内存(比如RAM)移动到图像内存(比如GPU中的VRAM)的位图图像(bitmapimage)。将其移至GPU后,您可以将其放入网格几何体中,并使用Chrome中的纹理从GPU中获取页面内容块。通过将纹理应用于非常简单的矩形网格,可以轻松匹配不同的位置和变换,这就是3DCSS的工作原理。不好理解,直接看例子。在chrome中,我们可以看到上面提到的GraphicsLayer——图层的概念。在开发者工具中,我们进行如下选择,调出显示图层边框选项:在一个非常简单的页面上,我们可以看到如下,这个页面只有一层。蓝色网格代表瓦片,你可以把它们看成是层(不是层)的单位,Chrome可以将它们作为更大层的一部分上传到GPU:创建元素自己的层,因为上面的页面非常简单,所以没有生成层,但是在非常复杂的页面中,例如,如果我们在一个元素上设置一个3DCSS属性来对其进行变换,我们可以看到该元素拥有自己的层时的样子。请注意橙色边框,它在此视图中勾勒出图层的轮廓:图层的创建何时触发?上图中黄色边框框起来的图层就是GraphicsLayer,它对我们的网页动画来说非常重要。通常,Chrome会将图层的内容绘制成位图,然后再将其作为纹理上传到GPU。如果内容不会改变,那么就不需要重新绘制(repaint)图层。这样做的意义在于,重绘所花费的时间可以用来做其他事情,比如运行JavaScript。如果绘制时间很长,也会造成动画失败和延迟。那么元素什么时候触发图层的创建呢?当前,如果满足以下任何条件,则会创建一个层:3D或透视变换(perspective、transform)使用加速视频解码的CSS属性具有3D(WebGL)上下文或用于2D上下文的加速元素混合插件(如Flash)CSS为它们自己的不透明度设置动画或使用动画转换元素具有加速的CSS过滤器元素有一个包含复合层的后代节点(换句话说,一个元素有一个子元素在它自己的层中)元素有一个较低的z-index兄弟元素包含复合层(即元素渲染在复合层之上)层的重绘是针对静态网页也就是第一次绘制后不会改变层,而是针对网页动画,页面的DOM元素是不断变化的,如果在变换过程中图层的内容发生变化,图层会被重绘(repaint)。强大的chromedevelopertools提供的工具可以让我们在动画页面运行的时候看到哪些内容被重绘了:在老版本的chrome中,有一个showpaintrects的选项,可以查看页面的哪些层重绘了重新绘制并标有红色边框。不过新版的chrome好像去掉了这个选项。当前选项是启用油漆闪烁。它的作用也是标记网站的动态变化,用绿色边框标记。看上面的示意图,可以看到页面上有几个绿色框,说明发生了重绘。请注意,Chrome并不总是重绘整个层,它会尝试智能地重绘DOM的无效部分。按照理论,如果页面上有这么多动画,重绘应该会很频繁,但是我在上图中的星球动画中只看到几个绿色的重绘框。我个人的理解是,一是GPU优化,二是如果整个动画页面只有一层,那么使用transform进行变换,页面必须重绘,但是使用了分层(GraphicsLayer)技术,即上述满足情况的元素创建自己的层,那么一个元素创建的层使用transform变换,比如rotate旋转,此时该层的旋转变换不影响其他层,所以这个图层不一定需要重绘。(个人意见,还望指正)。了解层重绘对于Web动画的性能优化至关重要。是什么导致失效强制重绘?这个问题很难详细回答,因为有大量的情况会导致边界失效。最常见的情况是操纵CSS样式来修改DOM或导致重排。找到重绘和回流根本原因的最佳方法是使用devtoolstimeline并启用paintflashing工具,并尝试找到在重绘/回流之前修改DOM的位置。总结那么浏览器如何将DOM元素显示到最终的动画中呢?浏览器解析HTML得到DOM并将其分成多个层(GraphicsLayer),为每一层的节点计算样式结果(Recalculatestyle--stylerecalculation)为每个节点生成图形和位置(Layout--reflowandre-layout)将每个节点绘制并填充到图层位图中(PaintSetupandPaint--redraw)将图层作为纹理(texture)上传到GPU将多个图层与页面一致,生成最终的屏幕图像(CompositeLayers--LayerReorganization)Web动画的很大一部分开销在于层的重绘,基于层的复合模型对渲染性能有着深远的影响。当不需要绘制时,合成操作的开销可以忽略不计,因此避免图层重绘是尝试调试渲染性能问题时的首要目标。那么这就为动画的性能优化提供了一个方向,减少元素的重绘和回流。回流(reflow)和重绘(repaint)这里首先要区分两个概念,重绘和重绘。Reflow(回流)当渲染树(rendertree)的一部分(或全部)由于元素的大小、布局和隐藏发生变化而需要重新构建时。这称为回流或重新布局。每个页面至少需要一次回流,即页面首次加载时。在回流期间,浏览器会将受影响的渲染树部分作废,并重建这部分渲染树。回流完成后,浏览器会将受影响的部分重新绘制到屏幕上,这个过程称为重绘。重绘(repaint)当渲染树中的某些元素需要更新属性,而这些属性只影响元素的外观和样式,而不影响布局,如background-color,就称为重绘。值得注意的是,回流一定会引起重绘,重绘不一定会引起回流。显然,回流更昂贵。简单地说,回流发生在操作元素导致元素修改其大小或位置时。当reflow被触发时:ResizingthewindowChangingthefontAddingorremovingastylesheetContentchanges,suchaswhentheuserentertextintheinputbox(内容变化,如用户在输入框中键入文本)激活CSS伪类,如:hover(在IE中激活兄弟节点的伪类)(CSS伪类的激活如:hover(在IE中激活兄弟节点的伪类)操纵类属性ScriptmanipulatingDOM(AscriptmanipulatingtheDOM)CalculatingoffsetWidthandoffsetHeightAttributes(CalculatingoffsetWidthandoffsetHeight)Settingapropertyofthestyleattribute(Settingapropertyofthestyleattribute)所以对于页面而言,我们的目标是最小化页面回流重绘,举个简单的栗子://下面这个方法会导致reflow回流两次varnewWidth=aDiv.offsetWidth+10;//ReadaDiv.style.width=newWidth+'px';//WritevarnewHeight=aDiv.offsetHeight+10;//ReadaDiv.style.height=newHeight+'px';//写//下面的方法比较好,只会回流一次varnewWidth=aDiv.offsetWidth+10;//ReadvarnewHeight=aDiv.offsetHeight+10;//ReadaDiv.style.width=newWidth+'px';//WriteaDiv.style.height=newHeight+'px';//写上面四句,因为涉及到offsetHeight操作,浏览器强制reflow两次,下面四句结合offset操作,所以减少页面的reflow一次,减少了reflow和重绘。其实需要减少reflow渲染树的操作(合并多个DOM和样式修改),减少对一些样式信息的请求,尽量利用好浏览器的优化策略。flushqueue其实是有针对浏览器本身的优化策略的。如果每一句Javascript都去操作DOM来回流重绘,浏览器可能会受不了。因此,很多浏览器都会对这些操作进行优化。浏览器会维护一个队列,把所有会引起回流和重绘的操作都放到这个队列中。当队列中的操作达到一定数量或达到一定时间间隔时,浏览器就会刷新队列并进行批处理。这会将多次回流和重绘变成一次回流重绘。但也有例外,因为有时候我们需要准确获取某些样式信息,如下:offsetTop,offsetLeft,offsetWidth,offsetHeightscrollTop/Left/Width/HeightclientTop/Left/Width/Heightwidth,height请求getComputedStyle(),或者IE的当前样式。这个时候浏览器为了反馈最准确的信息,需要马上回流重绘,保证给我们的信息是准确的,所以可能会导致flushqueue提前执行。display:none和visibility:hidden都隐藏页面上的节点。不同的是display:none隐藏的元素不占用任何空间。它的宽高等属性值会“丢失”visibility:hidden隐藏的元素空间依然存在。它仍然有高度和宽度等属性值。从性能的角度,即回流和重绘方面,display:none会触发reflow(回流)visibility:hidden只会触发repaint(重绘),因为找不到位置将两者都改成visibility:hidden在优化中会更好看,因为我们不会因此改变文档中已经定义的显示层次结构。对子元素的影响:display:none对父节点元素应用display:none后,父节点及其后代节点元素全部不可见,无论其后代元素如何设置display值,都无法显示;一旦visibility:hidden应用于父节点元素如果visibility:hidden被应用,它的所有后代也将不可见。然而,也有隐藏的“失败”。当visibility:visible应用于其后代元素时,后代元素将再次出现。动画性能检测与优化性能消耗样式不同样式消耗性能不同。比如box-shadow,从渲染的角度来说是非常耗性能的。原因是与其他样式相比,它们的绘图代码执行时间太长了。也就是说,如果需要频繁地重新绘制一个代价高昂的样式,那么您将遇到性能问题。其次,你需要知道没有什么是不变的,今天表现不佳的样式明天可能会得到优化,并且浏览器之间存在差异。所以关键是你用你的开发工具找出性能瓶颈,然后尽量减少浏览器的工作量。幸运的是,chrome浏览器提供了很多强大的功能,可以让我们检测我们的动画性能。除了上面提到的,我们还可以通过查看下面的showFPSmeter来显示页面的FPS信息和GPU的使用率:使用will-change提高页面滚动、动画等的渲染性能。官方文档说[4]这是一个仍处于实验阶段的特性,因此这个特性的语法和行为可能会在未来的浏览器版本中发生变化。will-change为web开发者提供了一种将元素发生变化通知浏览器的方式,以便浏览器在元素的属性真正发生变化之前,提前做好相应的优化准备。这种优化可以提前准备部分复杂的计算工作,让页面的响应更快更灵敏。看一下CaniUse-will-change[5],更新于2021/03/31:使用示例:(每个值的含义,去文档)will-change:autowill-change:scroll-positionwill-change:contentswill-change:transform//Exampleofwill-change:opacity//Exampleofwill-change:left,top//Exampleofwill-change:unsetwill-change:initialwill-change:inherit//example.example{will-change:transform;}值得注意的是,要很好地使用此属性并不容易:不要将will-change应用于太多元素:浏览器会尽力优化所有内容。还有一些更强大的优化,如果结合will-change,可能会消耗大量的机器资源。如果使用过度,可能会导致页面响应缓慢或消耗大量资源。谨慎使用:通常,当元素恢复到原始状态时,浏览器会丢弃之前的优化工作。但如果直接在样式表中显式声明will-change属性,则意味着目标元素可能会频繁变化,浏览器会比以前更长时间地保存优化工作。所以最好的做法是在元素变化前后通过script切换will-change的值。不要过早应用will-change优化:如果你的页面性能没有问题,就不要添加will-change属性来榨取一点速度。will-change最初被设计为尝试和修复现有性能问题的最后手段的优化。它不应用于防止性能问题。过度使用will-change会导致大量内存占用,并导致更复杂的渲染过程,因为浏览器会尝试为可能的更改做准备。这可能会导致更严重的性能问题。给它足够的时间来工作:页面开发人员使用此属性来通知浏览器哪些属性可能会更改。然后浏览器可以选择在更改发生之前提前做一些优化工作。因此,给浏览器一些时间来实际进行这些优化非常重要。使用的时候需要想办法提前一定时间知道元素可能发生的变化,然后给它加上will-change属性。GPU可以加速网页动画,上面已经反复提到了。3D变换会开启GPU加速,比如translate3D、scaleZ等。当然我们的页面可能没有3D变换,但不代表我们不能开启GPU加速。在非3D转换页面上使用3D转换也是一种hack。加速法。我们实际上不需要更改z轴,但我们假装声明它来欺骗浏览器。参考:Rendering:repaint,reflow/relayout,restyle[6]ScrollingPerformance[7]MDN--will-change[8]How(not)totriggeralayoutinWebKit[9]HighPerformanceAnimations[10]AcceleratedRenderinginChrome[11]用CSS3制作3D旋转球体[12]最后,本文到此结束,希望对你有所帮助:)参考资料[1]Demo-CSS33D行星运动:http://chokcoco.github.io/demo/css3demo/html/exampleSolarSystem.html[2]Demo-CSS33D行星运动:http://chokcoco.github.io/demo/css3demo/html/exampleSolarSystem.html[3]【CSS3进阶】炫酷的3D旋转视角:http://www.cnblogs.com/coco1s/p/5414153.html[4]官方文档说:https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-更改[5]我可以使用-will-change:https://caniuse.com/?search=will-change[6]渲染:重绘、回流/重新布局、重新样式化:http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/[7]滚动性能:http://www.html5rocks.com/zh/tutorials/speed/scrolling/[8]MDN--will-change:https://developer.mozilla.org/en-US/docs/Web/CSS/will-change[9]如何(不)触发布局inWebKit:http://gent.ilcore.com/2011/03/how-not-to-trigger-layout-in-webkit.html[10]高性能动画:http://www.html5rocks.com/en/tutorials/speed/high-performance-animations/[11]Chrome中的加速渲染:http://www.html5rocks.com/zh/tutorials/speed/layers/#disqus_thread[12]CSS3制作3D旋转球体:http://www.bluesdream.com/blog/css3-to-create-3d-rotating-sphere.html
