示例代码托管在:http://www.github.com/dashnowords/blogscss
博客园地址:《大史住在大前端》原创博文目录html
华为云社区地址:【你要的前端打怪升级指南】前端
[TOC]html5
动画的流畅程度一般是以FPS
(Frame Per Second,每秒帧率)做为衡量的。在摄像机录制视频时每一帧实际上包含了一段时间内的画面记录(长曝光摄影的道理相同的),若是画面里的事物在运动,那么暂停播放时看到的画面一般都是模糊的,这样的画面也被称为“模糊帧”,加上双眼“视觉暂留”效果的影响,影视做品通常只要达到24FPS
就能够展现出看起来连续运动的画面;而在页面的渲染中,每一帧都是由计算机计算渲染出来的精确画面,帧和帧之间并不存在模糊过渡,因此一般认为须要达到50FPS~60FPS
的帧率,才可以获得较好的观看体验。git
为了达到尽量接近60FPS
以上的帧率,浏览器每一帧的计算和绘制所花费的时间就须要控制在1000/60≈16.6ms之内,根据Google开发者社区提供的资料,开发者最好可以将全部的工做控制在10ms
左右,以便给浏览器一些处理内部工做的时间,不然就没法在限定的时间内完成画面更新,动态的内容就会表现出卡顿,对用户体验形成负面影响。下一节就来看一下,在这16ms
的时间里,浏览器都须要完成哪些任务。github
谈起浏览器的工做流程,你可能会在大多数文章中见过下面这张图:编程
它直观地描述了浏览器如何将HTML
文件和CSS
样式文件经过逐步处理最终合成渲染树并展现在页面上的过程,固然其中每一步都是很是复杂的,若是你对此还不熟悉,能够经过【浏览器的工做原理:新式网络浏览器幕后揭秘】这篇文章进行了解(极力推荐这篇文章!)。但实际上上面的流程里并无覆盖网站的整个生命周期,它只是描述了从用户获取到网站首页和资源文件后到完成首屏渲染这段时间内所作的工做,尽管工做流程几乎是一致的,但诸如响应用户的交互动做,在页面上实现动画等等内容,只经过上面的宏观原理图理解起来仍是很困难的。当开发者谈及浏览器渲染性能的话题时,咱们一般会听到“重排”、“重绘”等术语,实际上它们就是对这后半部分工做的描述,它被称为**“浏览器像素渲染管线”**,此时就须要祭出Google开发者社区提供的基本原理图:canvas
编写在JavaScript
代码中的那些事件监听器、定时任务等等异步触发的代码就会在橙色的部分执行,这部分代码运行在主线程中,若是有问题的代码或是执行时间较长的代码在其中形成了阻塞,后续的几个步骤就只能等着,这会直接延缓页面的渲染甚至致使页面直接崩溃,当JavaScript
执行完一个宏任务并清空了当前的微任务队列后,就会开始UI渲染流程,进入下一个环节。后端
在Style
阶段须要找出发生变动的样式并从新计算相关的尺寸,固然在首屏渲染以前第一次处理CSS
样式时,浏览器确定已经对计算结果进行了缓存,以便在这像素渲染管线处理时节省时间。数组
计算完样式自己后,就须要进入Layout
阶段,从新来计算发生样式变更的元素应该以怎样的盒模型尺寸绘制在画面上的哪一个位置,网页中的基本排版遵循正常文档流的规则,因此一个元素尺寸变化后,就有可能须要从新计算其父子元素或临近元素的位置,不难想象这是一个极容易引起蝴蝶效应的环节。完成了Layout
布局后,能够看到图中使用的颜色也发生了变化,由于相对而言它们的开销就比较轻量了。
Paint
阶段就是生成像素数据的过程,它会将元素的背景、边框、阴影等等可见的部分绘制出来,它们可能会被绘制在多个层上。
Composite
阶段,因为绘制阶段生成的画面可能分布于多个层,那么最终渲染的结果就须要将它们按照必定的顺序完成画面的重叠,这就是浏览在合成阶段主要的工做,固然这个过程并不必定是由CPU
独自完成的,后面还会讲到。当动画执行时,浏览器会不断建立帧,上面的过程就会反复发生,从而实现帧画面的不断变更:
不一样的CSS
样式的性能开销和形成的影响是不一样的,因此上面的像素渲染管路的各个阶段并不必定都有工做要作,若是发生变动的元素样式不会形成布局变化,那么layout
阶段就不须要作什么工做,若是发生变动的CSS
属性也能够不用从新计算各部分的像素颜色,那么paint
阶段也就没有什么工做要作,这样渲染管路就被简化成为:
这是咱们最指望获得的理想状态。若是发生变化的CSS
属性致使Layout
阶段任务量的增长,这类状况就被称为**“回流”或“重排”,若是发生变化的CSS
属性致使了Paint
阶段任务量的增长,这类状况就被称为“重绘”**,它的开销相比Layout
而言更小,从管线的特征不难明白,“回流”必然会致使“重绘”,但反之则不必定成立。
只经过Composite
阶段的工做就能够处理的CSS
属性就是**opacity
(透明度)和transform
(变形)**,它们是各种场景中优先推荐使用的性能最高的特性,transform
能够很方便地模拟出位置变化,在能够忽略画面精度的状况下(例如纯色的背景)也可使用scale
来模拟尺寸变化。
因此在知足需求的前提下,咱们固然但愿选择改变性能开销更小的属性,以即可以在16ms
的时间内完成整个渲染管线的任务,这里所说的性能,一般是指持续修改样式时的性能开销,暂不讨论低频的页面状态变更。关于CSS
属相详细的性能开销,能够在【CSS Triggers】查看详情,每一个浏览器的实现上有细微的差异。
opacity
和transform
的动画性能开销最小,并非由于处理它们形成的影响时工做量减少了,而是由于这两个属性形成的影响能够在图层合成时能够委托给强大的GPU
来执行。GPU
的基本架构和CPU
不一样,它拥有更多算术逻辑单元(也就是ALU
),这使得它很是适合以并行计算的形式执行计算密集型任务,例如图形的矩阵变换、人工神经网络的训练等等。
而opacity
和transform
形成的影响,均可以经过改变图层合成时的参数来进行处理,换句话说就是它能够直接使用以前生成的位图像素数据的缓存,而不须要再从新计算,也不用更新像素数据缓存,配合上GPU
强大的算力,性能天然很能打。
现代浏览器多采用软硬件混合渲染的方式来处理,软件渲染的方式一般也被成为“旧软件渲染”(与之相对应的是硬件加速渲染),“旧”只是出现时间比较早,并不表示它已经被硬件渲染所取代。最初的网页并非做为完整的应用存在的,而只是用来作一些信息展现,二维渲染的场景居多(由于页面上大多都是基于“盒模型”的矩形区域和文字包围盒的计算和绘制),这时使用CPU
渲染的性能并不低,“旧软件渲染”一般使用底层的二维图形绘制库,你能够借助HTML Canvas 2D API
来类比理解,在canvas
画板上实现的二维动画,即便在逐帧动画中进行覆盖式的全画布重绘,也可以保持较高的帧率;对3D图形学
有必定了解的小伙伴都知道,3D
渲染引擎只支持点、线和三角形的绘制,因此一个矩形就至少须要2个三角形来表示(固然也但是多个),直观感受上就是一种“杀鸡用牛刀”的体验,GPU
的算力虽然很牛逼,但一般内存空间很是有限,因此最好只在必要时有节制地使用GPU
。
本节咱们先忘掉GPU
的加速能力,来看看软件中须要如何处理页面渲染。下面以WebKit
内核为例来讲明一下渲染的基本处理过程以及建立合成层的条件。想要进一步了解的小伙伴能够尝试阅读朱永胜的**《WebKit技术内幕》**一书(不要轻易尝试,很容易以为本身不适合搞前端,甚至怀疑人生)。
在DOM
树解析时,浏览器会为可见元素建立一个RenderObject
类的实例,用于记录绘制这个节点须要的一些信息和方法,RenderObject
会依据HTML中的DOM结构生成一棵RenderObjectTree
,但浏览器并无直接使用它来生成一张位图画面,由于若是这样作的话,页面上发生任何变化时,都须要从新计算变动的区域并更新缓存,它的确很节省空间,毕竟只须要缓存一张静态图片中各个像素点的颜色数据就能够了,但节省空间的代价就是没法节省时间,这样的策略会加剧重复运算的负担。
为了方便处理,WebKit
会根据RenderObjectTree
来对RenderObject
进行按层分类,并最终建立一棵包含多个渲染图层信息的RenderLayerTree
(渲染层树),两棵树中的节点并非一一对应的,当遍历RenderObjectTree
时,只有符合必定条件的节点(好比获取了上下文的canvas节点、video节点、具备透明样式的节点等等,详细的规则会根据平台实现不一样可能会有变化)会建立出新的RenderLayer
节点,而其余的节点只须要添加到祖先节点上已经存在的RenderLayer
节点上就能够了。规则以下:
除了根节点之外,一个
RenderLayer
节点的父亲,就是它对应的RenderObject
节点的祖先链中最近的祖先,且二者所在的RenderLayer
不是同一个。
根据《Webkit技术内幕》一书中的介绍,在软件渲染中,每个RenderLayer
对象都会有一个后端类,用来存储该层绘制的结果(可是在硬件渲染中因为合成层的存在,因此并不会为每个RenderLayer
生成后端类),你能够把后端类简单地理解为结果缓存,CPU
会将各个RenderLayer
的结果最终渲染为到一张位图里,而后交给GPU
展现,合成的过程也能够在GPU
中进行,也就是硬件加速渲染,这里再也不展开,可是仅考虑软件渲染环节的话,RenderLayer树就已经能够实现目的了。用过photoshop
的用户可能会对分层这种处理形式比较熟悉,它的关键点就是在处理有重叠的区域时必须考虑前后顺序。
直接看概念可能比较绕,作个简单的比喻,好比码农小强的爷爷有本身的房子,而后生了几个孩子,这些孩子里有的发展的比较好就本身买房单独住处去了,发展的不太好的只能住在爷爷家里,接着每一个孩子又生了一堆孩子,也就是小强这一辈,固然也是发展的有好有差,以码农小强为例,发展的好的就能够本身买房子住,发展的很差的就得拼爹了,若是他爹有房子,就能够住在爹家,若是很悲剧他爹也没房子,那他就得和他爹一块儿住到他爹的爹家里去(说住到坟墓里的你放学别走),RenderObject
到RenderLayer
的生成过程也是相似的。
Webkit
底层的2D
渲染使用Skia
库,它是相似于Canvas API
的二维图形绘制库,为了方便理解软件渲染的优点,下面经过Canvas API
来看看分层到底带来了哪些变化,本例中咱们先不考虑从新计算布局的状况,仅考虑重绘的工做。如下图为例(若是不了解canvas
动画绘制,能够参考笔者曾经写的一篇相关博文【响应式编程的思惟艺术 (2)响应式Vs面向对象】):
假设在下面的分析中,地面
、天空
、山
、云
和人
是分别绘制上去的,人物和云是能够水平运动的,人比山距离观察者更近。
在canvas
中,使用context.getImageData(x, y, width, height)
方法取得画布上对应矩形区域的像素数据,在不分层的状况下,假设第一次渲染后,使用这个方法将画布中的像素数据取出来存储在backUp
变量上(像素数据是一个很长的一维数组,按顺序逐行存储着画面中每一个像素点的rgba
4个值),也就是只为最终结果创建了一份缓存,此时实际上已经丢失了一部分信息了,例如云和天空、人和天空都有重叠的部分,而重叠部分的像素只保留了最上面一层的值。
当须要绘制逐帧动画时,问题就来了。人物是运动的,那么程序天然知道下一帧应该将人物绘制在什么地方,可是若是直接绘制,原来的人物仍然会留在图中,这样逐帧画下去,画面上就会留下一排人物运动的分解画面,这显然是不行的;若是把人物先擦掉呢?也是不行的,这样虽然能够保持画面上只有一个跑动的人物,可是由于画面被缓存时,像素已经被覆盖掉了,若是把人物擦掉,只从缓存的数据中,是没法知道被擦掉的这部分像素点应该被修复成什么样子的,例以下图中,缓存中是上一帧的数据复原后的图,可是若是下一帧人物离开了原位置,原来的画面就没法利用缓存直接恢复了,例如上图中红框中的部分就留下了人物的残影。
假设在上面的画面中,人物的大小是100*100
,缓存的像素中,其位置是(200,400)
,假设一帧中它平移了10
个像素,那么就能够粗略地认为须要更新的区域是左上角为(200,400)
,宽110
,高100
的矩形区域。尽管这个110*100
的矩形区域可能只占了整个缓存区域的10%
,也就是大部分缓存的像素点仍是有效的,但为了修复这部分画面,程序将不得不从新计算每一个对象的绘制结果,而后将这个区域的画面按照层次从新绘制上去,在上面的示例中,变动区擦除后从下到上依次要绘制天空、山和人物,人物是绘制在最上层的以即可以完整显示,人物离开后的空白像素也在重绘中被修复。
单幅位图像素缓存的劣势其实已经很明显了,下面再来看看分层的状况,假如上述画面中的对象分别绘制在不一样的canvas
画布上,那么一共就须要5个canvas
元素,因为画布是透明底色的,因此最终显示结果是叠加而成的。接着为每一个canvas
层都生成像素数据的缓存,那么在面对一样的更新场景时,天空、地面、山和云均可以不用操做,而只须要更新人物所在的canvas
层,先将受影响的区域擦除,接着从新计算人物的绘制结果并更新单层的缓存,最后将新的结果绘制到目标位置上,相比之下,分层缓存的方案使用了更多的存储空间来缓存绘制的像素数据,但减小了更新时的计算量,是典型的空间换时间的作法。
显示器上最终呈现的是一幅位图画面,因此即便在上面的示例中使用了5个分布在不一样层次的canvas
标签,实际上计算机在处理时仍然会对各层的像素数据按层进行合并计算。上面的示例中存在一个很容易发现的优化点,就是不管怎么重绘,实际上山
和地面
的绘制结果都会挡住对应区域的天空
的绘制结果,并且它们都是静态的,因此天空
的缓存数据中,与山
和地面
重叠的部分实际上没什么用,若是更新的区域发生在重叠区,那么更新画面的时候,天空
层老是要先绘制一次而后再被更高层的山
或者地面
覆盖掉,这时候就能够利用层合并的思想进行优化,也就是直接将天空,山和地面绘制在同个canvas
上,它们总体的绘制结果缓存时只须要占用原来1/3的空间(3张位图变1张了),但对于后续的重绘却不会形成影响,这样就能够省掉很大一部分肯定没有用的缓存。固然上面的示例只是比较简单的状况,在DOM节点渲染结果的处理时有更加复杂的层划分和层合并的规则,可是优化的思想基本是同样的。
从直接绘制到分层绘制再到层的合并的过程,实际上就是从DOM节点到RenderObject树
再到RenderLayer树
的变换过程,利用canvas
的实例就比较容易理解软件渲染过程当中的一些策略了,不少东西你以为不理解,并不必定是由于它自己有多复杂,只是由于你没法知道它是为了解决什么问题而存在的,实际上当你面对一样的问题时,可能也会采起相似甚至更好的处理策略,但当咱们只看别人描述解决方案时,一般都会感受到一个东西“特别复杂”或者“特别高大上”,因此请永远保持谦逊,但也别丢了你的自信。最后分享一个最近很喜欢的冷段子,下一期再见。
问:"从前有一只菜鸟,他特别菜,可是他仍然在飞,请问为何?"
答:“由于他有一颗勇敢的心!”
原文出处:https://www.cnblogs.com/dashnowords/p/11706774.html