本文内容:为何我们力求达到60帧/秒的目标,浏览器渲染每一帧涉及哪些内容,css的不同属性为何会以不同的方式影响性能。每帧里,浏览器是如何花费时间的。如何利用浏览器查找性能瓶颈。
APP的生命周期(RAIL)
我们称应用的生命周期四大领域:response、animate、idle、load简称RAIL。
初始加载应该在1秒内完成,当加载完成,通常处于闲置状态,等待用户操作,这时候浏览器就抓住处理那些为了满足1s完成加载目标而被推迟的工作,通常这些闲置时间约为50ms,虽然一次可能出现多个闲置时间段,这些闲置时间段是完成繁杂任务的绝佳机会。以便用户做出互动时一切都顺畅快速。
那么响应程度要达到什么级别呢?研究表明存在100ms上限,当用户按下屏幕上某项内容后更具有挑战性的是此时需要实现动画效果并且需要达到60fps,1000ms/60=16ms,实际上,因为浏览器也需要处理时间,所以只有10-12ms的时间。
关键渲染路径
如今多数设备刷新屏幕的频率都是60帧/秒,如果浏览器花费太长时间才能显示一帧,就会丢失一帧,帧速度就会下降,用户就看到卡顿现象。如果情况很糟糕的话,整个屏幕就会卡住。
如果你不知道浏览器如何渲染帧的话,则无法优化应用的帧率,所以你需要了解当网页被加载后是如何显示在屏幕上的,现在我将简单介绍这个流程。
构建DOM树
首先浏览器向服务器发出获取请求,服务器作出响应并返回html,此时浏览器采用非常智能的措施并提前解析,生成DOM树。
计算样式
根据css生成CSSOM树,计算每个元素的样式。
计算布局(layout)
计算dom树中每个节点的几何信息,即计算每个元素的占用空间,位于屏幕的什么位置
dom结合css,获得新的树,叫做渲染树,也称布局树。
如果css设置display:none,那么将会从渲染树里移除。
绘制
执行绘制
合成和显示
当完成后,效果是这样的
单个帧的内容
通常单个帧包含以下内容
css如何影响帧的内容
重排:更新元素的几何属性。比如元素的宽度、高度等,浏览器会触发重新布局,解析之后的一系列子阶段。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
重绘:更新元素的绘制属性。比如背景颜色,省去了布局,帧是这样的:
直接合成阶段:使用css的transform来实现动画,这可以避免重排和重绘,直接在非主线程上执行合成动画操作。这样效率是最高的,因为在非主线程上合成,并没有占用主线程的资源,最后帧是这样的:
所以不同的样式会影响帧的生成,进而影响性能。
查看单帧内容
在开发者工具中,每帧里浏览器是如何花费时间的 。
找到开发者工具-performance一栏,点击录制,浏览器收集页面信息生成两种视图。
flame chart(火焰图)
图标从上往下延伸,如果管道的某个部分触发了其他内容,则在父记录的下方显示子记录。
放大这部分后,你会看到这里触发了动画帧,调用了其他javascript,导致了calc style、导致了layout。你可以明白哪些事件导致了哪些事件。
瀑布视图
可以看到是什么操作,时长多少、开始时间,然后再稍微细分了下、时间本身和任何子记录,最后看到任务是在代码的何处触发。
recalculate style会显示受到影响的元素数量。
layout会显示树的大小,范围,开始位置、代码何处触发了布局。
找出有问题的代码,分析性能瓶颈
案例一:点我
该网站的记录看起来非常接近60帧/秒。但并非完全达到了,所有紫色柱表明可能出现了太多的布局事件。放大后很明显能看出这些布局存在问题。
warning:Force synchronous layout is a possible persormance bottleneck
来自脚本quiet的第172行,罪魁祸首就是它。点击查看导致强制布局的函数。
打开看到了,每帧中由onSwitchLayoutClick调用的函数导致的。修改元素class,该class修改元素的宽度,导致重排,触发浏览器重新布局。
案例二:点我
我们在时间线里看到了很多绿色柱状,明显存在绘制问题,放大其中一帧看看
看来每帧以脚本开始,有个Animation Frame Fired事件,紧着着是样式计算和绘制事件,似乎是JavaScript问题,因为如果问题来自css,那么就不会看到Animation Frame Fired事件。
RequestAnimationFrame
动画的时间上限16ms,实际上只有约10ms的时间来执行所有操作并准备好帧包括运行布局、合成和绘制。
RequestAnimationFrame这个API可以让你的JavaScript在每帧的合适时间运行。RequestAnimationFrame应该是你创建动画时的必备工具。要达到60fps浏览器根本没有渲染时间,在渲染每一帧的同时,浏览器还有一些额外的工作要做,因此我们应该控制在10ms之内完成渲染工作。
一帧的JavaScript部分通常应该最长保持在3到4毫秒,因为之后还有其他工作,比如样式计算,图层管理和渲染层合并。
假设有关样式的工作,然后出现需要处理的JavaScript,在继续处理其他任务之前,浏览器需要处理插进来的JavaScript,新来的JavaScript可能导致该帧的工作重新反工。可能导致丢失了这一帧。
所以尽早在每一帧的开始执行。这样尽量给浏览器留出足够的时间来运行代码。
网络上很多用来实现动画的旧版代码使用setTimeout。
这两个函数存在的问题是JavaScript引擎,安排这两个函数根本不会关注渲染管道,如果你想等待一段时间或经常重复执行某些任务,这两个函数派上用场,但是它们不适合动画。
RequestAnimationFrame用法
调用它,并告诉它要调用哪个函数,在RequestAnimationFrame结束时再安排下个动画。浏览器会知道何时运行,如何运行。IE9不支持
强制同步布局FSL
如果你触发了强制同步布局,在帧视图中,你会在布局记录的右上角看到红色的三角形。
找出问题代码:
浏览器必须计算offsetWidth,这就需要布局。每次更改样式,刚刚执行的布局流程都会变得无效,因为你更改了样式,现在浏览器就要重新完成一遍,这一错误的代码很高。
布局跑到计算样式的前方
点我查看哪些css会触发布局。
避免FSL
在JavaScript阶段先读取布局属性,意味着你将使用上一帧的布局,然后进行所有样式更改。
自行查看how-not-to-trigger-layout-in-webkit
css选择器对性能的影响
发现Recalculate Style 的 self time时间太长,根本达不到60帧/秒。
当页面有大量的元素,类似:nth-child复杂的css选择器会给浏览器增加工作量。选择器越复杂,浏览器就更需要多次在DOM树上上下下移动,移动次数越多,找到正确元素花费的时间就越长。
解决办法:
- 减少受影响的元素
- 降低选择器复杂性
bem即块元素修饰符。它会对样式元素使用单个类的名称。并且对性能更有优势。因为对现代浏览器来说,通常类匹配是最快选择器。
Block
Element
Modifier
例如:.box--three
更多有关块元素修饰符的信息: