深度剖析浏览器渲染性能原理,你到底知道多少?
渲染卡顿是怎么回事?
网页不仅应该被快速加载,同时还应该流畅运行,比如快速响应的交互,如丝般顺滑的动画等。
大多数设备的刷新频率是60次/秒,也就说是浏览器对每一帧画面的渲染工作要在16ms内完成,超出这个时间,页面的渲染就会出现卡顿现象,影响用户体验。
为了保证页面的渲染效果,需要充分了解浏览器是如何处理HTML/JavaScript/CSS的。
渲染流程分为几步?
1、JavaScript:JavaScript实现动画效果,DOM元素操作等。
2、Style(计算样式):确定每个DOM元素应该应用什么CSS规则。
3、Layout(布局):计算每个DOM元素在最终屏幕上显示的大小和位置。由于web页面的元素布局是相对的,所以其中任意一个元素的位置发生变化,都会联动的引起其他元素发生变化,这个过程叫reflow。
4、Paint(绘制):在多个层上绘制DOM元素的的文字、颜色、图像、边框和阴影等。
5、Composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。
结合渲染流程怎么优化渲染性能呢?
结合上述的渲染流程,我们可以去针对性的分析并优化每个步骤。
1、优化JavaScript的执行效率
2、降低样式计算的范围和复杂度
3、避免大规模、复杂的布局
4、简化绘制的复杂度、减少绘制区域
5、优先使用渲染层合并属性、控制层数量
6、对用户输入事件的处理函数去抖动(移动设备)
优化JavaScript的执行效率,具体可以做什么?
1、动画实现,避免使用setTimeout或setInterval,尽量使用requestAnimationFrame。
setTimeout(callback)和setInterval(callback)无法保证callback函数的执行时机,很可能在帧结束的时候执行,从而导致丢帧。requestAnimationFrame(callback)可以保证callback函数在每帧动画开始的时候执行。
2、把耗时长的JavaScript代码放到Web Workers中去做。
JavaScript代码运行在浏览器的主线程上,与此同时,浏览器的主线程还负责样式计算、布局、绘制的工作,如果JavaScript代码运行时间过长,就会阻塞其他渲染工作,很可能会导致丢帧。
前面提到每帧的渲染应该在16ms内完成,但在动画过程中,由于已经被占用了不少时间,所以JavaScript代码运行耗时应该控制在3-4毫秒。
如果真的有特别耗时且不操作DOM元素的纯计算工作,可以考虑放到Web Workers中执行。
3、时间分片,把DOM元素的更新划分为多个小任务,分别在多个frame中去完成。
由于Web Workers不能操作DOM元素的限制,所以只能做一些纯计算的工作,对于很多需要操作DOM元素的逻辑,可以考虑分步处理,把任务分为若干个小任务,每个任务都放到requestAnimationFrame中回调执行。
降低样式计算的范围和复杂度,具体可以做什么?
添加或移除一个DOM元素、修改元素属性和样式类、应用动画效果等操作,都会引起DOM结构的改变,从而导致浏览器需要重新计算每个元素的样式,对整个页面或部分页面重新布局,这就是所谓的样式计算。
样式计算主要分为两步:创建一套匹配的样式选择器,为匹配的样式选择器计算具体的样式规则。
1、降低样式选择器的复杂度,尽量保持class的简短,或者使用Web Components框架。
2、减少需要执行样式计算的元素个数。
由于浏览器的优化,现代浏览器的样式计算直接对目标元素执行,而不是对整个页面执行,所以我们应该尽可能减少需要执行样式计算的元素的个数。
避免大规模、复杂的布局,具体可以做什么?
布局就是计算DOM元素的大小和位置的过程,如果你的页面中包含很多元素,那么计算这些元素的位置将耗费很长时间。
布局的主要消耗在于:1. 需要布局的DOM元素的数量;2. 布局过程的复杂程度。
1、尽可能避免触发布局,引发DOM回流。
当你修改了元素的属性之后,浏览器将会检查为了使这个修改生效是否需要重新计算布局以及更新渲染树,对于DOM元素的“几何属性”修改,比如width/height/left/top等,都需要重新计算布局。
2、避免强制同步布局事件的发生。
前面提过,将一帧画面渲染的屏幕上的流程是:首先是JavaScript脚本,然后是Style,然后是Layout,但是我们可以强制浏览器在执行JavaScript脚本之前先执行布局过程,这就是所谓的强制同步布局。
在JavaScript脚本运行的时候,它能获取到的元素样式属性值都是上一帧画面的,都是旧的值。因此,如果你在当前帧获取属性之前又对元素节点有改动,那就会导致浏览器必须先应用属性修改,结果执行布局过程,最后再执行JavaScript逻辑。
简化绘制的复杂度、减少绘制区域,具体可以做什么?
绘制就是填充像素的过程,通常这个过程是整个渲染流程中耗时最长的一环,因此也是最需要避免发生的一环。
如果Layout被触发,那么接下来元素的Paint一定会被触发。当然纯粹改变元素的非几何属性,也可能会触发Paint,比如背景、文字颜色、阴影效果等。
1、提升移动或渐变元素的绘制层。
绘制并非总是在内存中的单层画面里完成的,实际上,浏览器在必要时会将一帧画面绘制成多层画面,然后将这若干层画面合并成一张图片显示到屏幕上。
这种绘制方式的好处是,使用transform来实现移动效果的元素将会被正常绘制,同时不会触发其他元素的绘制。
2、减少绘制区域。
浏览器会把相邻区域的渲染任务合并在一起进行,所以需要对动画效果进行精密设计,以保证各自的绘制区域不会有太多重叠。
3、简化绘制的复杂度。
可以实现同样效果的不同方式,我们应该采用性能更好的那种。
优先使用渲染层合并属性、控制层数量,具体可以做什么?
1、使用transform/opacity实现动画效果。
使用transform/opacity实现动画效果,会跳过渲染流程的布局和绘制环节,只做渲染层的合并。使用transform/opacity的元素必须独占一个渲染层,所以必须提升该元素到单独的渲染层。
2、提升动画效果中的元素。
应用动画效果的元素应该被提升到其自有的渲染层,但不要滥用。
在页面中创建一个新的渲染层最好的方式就是使用CSS属性will-change,对于目前还不支持will-change属性、但支持创建渲染层的浏览器,可以通过3D transform属性来强制浏览器创建一个新的渲染层。需要注意的是,不要创建过多的渲染层,这意味着新的内存分配和更复杂的层管理。
尽管提升渲染层看起来很诱人,但不能滥用,因为更多的渲染层意味着更多的额外的内存和管理资源,所以当且仅当需要的时候才为元素创建渲染层。
对用户输入事件的处理函数去抖动(移动设备),具体可以做什么?
用户输入事件处理函数会在运行时阻塞帧的渲染,并且会导致额外的布局发生。
1、避免使用运行时间过长的输入事件处理函数。
理想情况下,当用户和页面交互,页面的渲染层合并线程将接收到这个事件并移动元素。这个响应过程是不需要主线程参与,不会导致JavaScript、布局和绘制过程发生。
但是如果被触摸的元素绑定了输入事件处理函数,比如touchstart/touchmove/touchend,那么渲染层合并线程必须等待这些被绑定的处理函数执行完毕才能执行,也就是用户的滚动页面操作被阻塞了,表现出的行为就是滚动出现延迟或者卡顿。
2、避免在输入事件处理函数中修改样式属性。
输入事件处理函数,比如scroll/touch事件的处理,都会在requestAnimationFrame之前被调用执行。
因此,如果你在上述输入事件的处理函数中做了修改样式属性的操作,那么这些操作就会被浏览器暂存起来,然后在调用requestAnimationFrame的时候,如果你在一开始就做了读取样式属性的操作,那么将会触发浏览器的强制同步布局操作。
3、对滚动事件处理函数去抖动。
通过requestAnimationFrame可以对样式修改操作去抖动,同时也可以使你的事件处理函数变得更快。