从前端角度看网页渲染慢的原理及解决方案
1.网页的写法导致渲染时产生的平台相关的layer过多。
浏览器在网页排版的时候会产生一棵render tree用于渲染。由于z-index和一些特殊元素如overflow,fixed元素等的存在,浏览器为了保证渲染的正确,真正渲染的时候不直接使用render tree,而是根据render tree的信息产生一棵对应的renderLayer tree。RenderLayer可以理解为是一个平面,上面可能对应着一个或者多个render tree上的节点,而对应当前主流的支持硬件加速的浏览器而言,还要基于renderLayer tree生成对应的graphicsLayer tree,那么在不同的平台上这棵graphicsLayer tree又对应于平台相关的实现,在android平台上就是layerAndroid tree了。layerAndroid tree有两棵,一个在内核线程,负责收集内核线程排版后产生的renderLayer tree的经过处理的信息,另一个在UI线程,由内核线程的layerAndroid tree复制得到,真正渲染的时候就是通过遍历这个UI线程的layerAndroid tree来实现,由于每个layerAndroid分开渲染之后再进行组合,所以一个layerAndroid的内容改变并不会影响到其他layerAndroid,所以网页部分内容的修改就不会导致整体网页的重绘了。因此,为某些特别的网页元素产生layer,会提高网页的渲染效率。但是有些时候过犹不及,如果网页产生的layerAndroid过多,遍历和绘制layerAndroid tree的时间就会变得非常长,这个时候就会出现渲染变慢和卡顿的问题了。而layerAndroid tree基本上是与graphicsLayer tree对应的,所以我们可以通过追查graphicsLayer的产生原因来分析layer过多导致的卡顿原因。
接上面,layerAndroid tree是根据graphicsLayer tree生成的,而graphicsLayer tree是根据renderLayer tree生成的。为了网页上一些特殊元素的渲染正确,两棵树上的节点并不是一一对应的,renderLayer tree上的节点可能不产生graphicsLayer,也可能产生不止一个graphicsLayer。
1.1 renderLayer在以下条件下会产生对应的graphicsLayer:
1> video元素
2>canvas元素
3>插件如flash元素
4>frame或iframe元素
5>网页元素有一些3d变换属性如translate3d,Preserve3D等
6>网页元素有backface-visibility:hidden属性
7>网页元素正在进行加速动画
8>网页使用了CSS Filter
9>Fixed元素
10>网页元素有overflow:scroll或者overflow:auto(产生滚动条的情况,与overflow:scroll一样)
11>网页元素使用了fixedBackgroundImage,这种情况会产生不止一个graphicsLayer
12>由于z-index的原因,如果一个产生了graphicsLayer的layer的z-index比当前的某个位置上部分或全部遮盖了该renderLayer的renderLayer的z-index低的话,我们称这种产生layer的情况叫做overlap。(简单来说,就是如果一个renderLayer盖在另一个renderlayer上面,如果下面那个产生了graphicsLayer,那么上面的那个也必须产生)
通过上面的介绍,大概可以了解到了渲染时候的layer的对应关系和产生原因了,下面说一下网页渲染卡顿的一些原因,可以分为几类:
1.2 layerAndroid tree过于复杂会导致渲染时候遍历的时间变长而导致渲染效率降低并且划屏卡顿。
这么多条件都可能生成grapicsLayer,我们就知道了,如果是前端工程师一个不注意,错误的乱用了上面的那些元素的一个写法,就会导致网页渲染的时候产生大量的layerAndroid,导致卡顿了。下面是需要注意的一些情况,这些情况都是我处理渲染卡顿问题的时候曾经遇到过的一些网页写法:
1>乱用translate3d属性
如果你不想做动画的话,不要在某个元素上乱用3d变换这个属性,包括translateZ,rotateX(YZ),Preserve3D等,见过好多网页的写法在某个div上面使用了translate3d(0,0,0)这个属性,然后又没有通过JS去修改这个属性,那么我就想问了,你既然不做动画,你写这个干吗,平白多产生一个layer,而且因为产生了这个layer,导致覆盖在他上面的一些小元素因为overlap都生成了graphicsLayer,一下子多了许多的layer,不卡就怪了,这种网页的写法见过很多,大家一定要注意。
2>乱用canvas属性
见过一些网页,如一些app和游戏分发网页,上面的一些网页条目和小图标等喜欢使用canvas来实现,对于这种网页的写法我想说,可能你了解到浏览器对于canvas会进行加速渲染的,但是对于哪些小的canvas,浏览器是不会为你加速的,浏览器对于canvas的渲染加速是有size判断的,过大和过小的都不能加速。这样既无法加速,又产生大量的layer,会导致渲染变慢。因此,同样使用div就可以做到的事情,就不要使用canvas等其他一些奇怪的东西了。
3>使用一些z-index为负或者低于body的z-index的fixed元素
这些fixed元素根本看不见,但是会生成graphicsLayer,网页上面的大量的元素都会因为这个fixed元素的overlap而产生graphicsLayer导致卡顿。所以如果你想使用fixed元素,那么保证他的z-index比body高。
一般在body上使用,当然也可以在其他元素上使用,我想说的是,使用这个属性并没有错误,不过因为浏览器实现的原因,这种属性的网页注定渲染效率不高,因为首先这个属性的元素要生成两个graphicsLayer,而因为两个graphicsLayer的位置非常低,所以和3的情况一样,上面的元素都会因为overlap产生比较多的graphicsLayer,所以渲染效率会下降。还有一个信息是,ios上的浏览器处于性能的考虑都是不支持这个属性的,只有android平台支持,但效果并不好。所以我的建议是,这个属性很鸡肋,能不用尽量不用。
5>使用backface-visibility:hidden属性
关于这个属性,我想说的是,如果不确定你的网页需要非常炫的3d旋转效果而必须使用这个才能达到效果的话,没必要使用这个属性。
6>ToDo
2. 一些特殊元素的写法导致卡顿:
某些特殊的元素因为浏览器实现的原理,导致不同的写法上会出现比较大的渲染效率差距,下面是需要注意的几种情况:
2.1 关于overflow:scroll和overflow:auto和可滚动的iframe
浏览器在实现overflow:scroll和overflow:auto的时候使用scrollableLayer,这种layer的划屏实现与普通网页的划屏实现不同,scrollableLayer的要复杂一些,效率的话相对低一些,但还算理想。但android浏览器对于这个layer的实现问题比较多,如果浏览器不支持硬件加速的话(4.0以下的android浏览器都不支持硬件加速),这个layer的渲染效率就会非常的低,所以使用这两个属性的时候一定要注意根据系统的版本来判断,iframe中的滚动内容也面临同样的情况,因此,对于这种写法,4.0以上的可以使用,4.0以下的机器一定要换一种实现方法。
2.2 关于CSS动画的实现
CSS动画分为两种,一种是可以加速的,一种是不可以加速的。对于android浏览器,只有动画的属性为transform和opacity的情况,才是可以加速的。加速动画的渲染过程不需要内核线程参与提供新的内容更新给UI线程来渲染,仅通过UI线程在渲染的时候进行一些矩阵和透明度变换就可以实现,效率比不可以加速的动画要高很多。不可以加速的动画需要内核线程不断的提供新内容给UI线程,然后UI线程渲染,然后再等待内核线程更新内容,这样两个线程同步采用动画的渲染,效率会比较低。
了解了动画的原理,我们就能理解下面关于网页动画写法的原则了:
1.尽量使用加速动画
一些动画的实现既可以使用加速动画,也可以使用非加速动画实现,显然应该使用加速动画,看下面的例子:
example1:
<html>
<head>
<style type="text/css">
#demo
{
position:absolute;
left:0px;
}
</style>
<script type="text/javascript">
var timer=null;
function startMove()
{
if(!timer)
{
timer=setInterval(move, 20);
}
}
function move()
{
var div=document.getElementById('demo');
var iSpeed=10;
if(div.offsetLeft>=200)
{
clearInterval(timer);
timer=null;
}
else
{
div.style.left=div.offsetLeft + iSpeed + 'px';
}
}
</script>
</head>
<body onload="startMove()">
<div style="border: 3px solid #C9C9C9; width:200px; height:200px;">
<div id="demo" style="border: 2px solid #000000; width:50px; height:50px;">
</div>
</div>
</body>
</html>
example2:
<html>
<head>
<style type="text/css">
div.demo
{
-webkit-transition-property: margin-left;
-webkit-transition-duration: 500ms;
-webkit-transition-timing-function: linear;
margin-left: 0px;
}
div.demo
{
margin-left: 200px;
}
</style>
</head>
<body>
<div style="border: 3px solid #C9C9C9; width:800px; height:800px;">
<div style="border: 2px solid #000000; width:600px; height:600px;" onclick="className='demo'">
</div>
</div>
</body>
</html>
example3:
<html>
<head>
<style type="text/css">
div.demo
{
-webkit-transition-property: -webkit-transform;
-webkit-transform-style: preserve-3d;
-webkit-transition-duration: 800ms;
-webkit-transition-timing-function: linear;
-webkit-transform: perspective(100px) rotateX(0deg);
-webkit-backface-visibility:hidden;
}
div.demo
{
-webkit-transform: rotateX(180deg);
}
</style>
</head>
<body>
<div style="border: 3px solid #C9C9C9; width:800px; height:800px;">
<div style="border: 2px solid #000000; width:600px; height:600px;" onclick="className='demo'">
Hello World!!!
</div>
</div>
</body>
</html>
浏览器对于上面的3个例子的效率怎么样呢?
第一个,使用JavaScript的timer来修改某个元素的left属性来实现动画,这种效率最低,因为浏览器解析和执行JavaScript的代码是需要时间的,这就导致两个问题:1.JavaScript的timer不准确,也就会导致动画的渲染时间不均匀。2.JavaScript的执行浪费了时间。这两点都会导致例子1的动画效果不好。
第二个,使用了transition方法来修改某个元素的margin-left属性来实现动画,这种的话,效率比第一个肯定要高,因为减少了JavaScript的处理时间,但margin-left属性不能使用到加速动画,前面已经介绍过了,不能加速的动画在渲染效率上是比较差的。
第三个,使用了webkit-transform来实现动画,前面说过了,transform属性可以被浏览器在渲染的时候进行加速,所以这个动画的效率最高,效果最好。
通过上面的对比,应该明白了,如果你想实现一个位置移动的动画,使用-webkit-transform吧,其他的方法都是垃圾。
2.3 关于JavaScript修改translate来实现模仿网页惯性滚动的方法
在我处理网页卡顿问题的过程中,遇到了一类网页,写法都是通过JavaScript来不断的修改网页的translate来实现一个类似于滑动普通页面时候惯性滚动的效果。我想这种写法应该是为了实现一个滚动普通网页无法实现的拖动到网页底部的回弹效果吧。但是这种网页让我非常的头疼,这种网页的渲染流程是这样:1.JavaScript监听touch事件,当收到一个touchStart或这touchMove的时候记录一下touch位置,设置为动画的translate的起始位置,然后监听到下一个touchMove的时候,做一些判断,如果判断为划屏操作的话,会记录下这个touchMove的位置,作为动画translate的终止位置,然后设置一系列动画的属性,触发动画。这是一个动画的周期。
首先,javaScript的参与降低了动画的效率,其次划屏的过程中需要不断的产生新的动画,虽然动画的执行过程在UI线程加速渲染,但动画添加的过程还是需要内核线程进行一系列计算和设置的,并且需要内核线程启动一次contentDraw来触发UI线程对动画的渲染,这个也是非常消耗时间的。所以综合上面的两个原因,这种页面的划屏桢率要比普通页面差了不少。所以你如果网页内容复杂并且也非常的长,不建议使用这种写法,写成普通网页不使用动画效果会更好。
其次,如果你必须使用JavaScript的动画,那么需要讨论一下translateY和translate3d的区别。对于浏览器来说,translateY是2d的变换,translate3d是3d的变换。这两种变换的区别在于浏览器会为3d变换的元素在执行动画之前就生成一个graphicsLayer, 并且动画结束后这个layer也不会被删除。而对应2d变换,仅仅在一次动画的周期中才会产生graphicsLayer,动画结束马上去除。而频繁的创建和删除layer是需要开销的。所以如果你想使用JavaScript修改translate来实现模仿网页惯性滚动,那么这个划屏的过程中就会出现大量的Y轴动画,这些动画会频繁的创建和删除,如果你写成translateY的2d变换,就会出现大量的graphicsLayer的创建和删除动作,开销很大,很容易造成卡顿。所以,这种情况下,务必使用translate3d。
2.4关于圆角矩形和box-shadow
skia在渲染圆角矩形的时候最终使用drawPath方法来实现,而普通矩形使用drawRect来实现,这两个方法的执行时间drawPath >> drawRect,两者差距十倍以上,特别是比较大面积的圆角矩形,差距更大,所以网页元素特别是面积比较大的尽量少使用圆角矩形。而渲染box-shadow的时候相当于需要渲染一个面积等同于原元素大小的一块区域。如果你的网页已经比较复杂了,那么请减少圆角矩形和box-shadow的使用。
2.5 过多的GIF
浏览器在处理GIF动画的时候,会重新发起一次重绘流程,过多的GIF会导致浏览器频繁重绘,使得滑动效率大大降低,所以应该限制页面上的GIF数量。
3 关于网页写法的一些原则和建议
3.1 上面的内容主要是针对一些细节点的建议,下面的是关于整体网页的写法的一些基本原则:
1>网页上不要使用太多动画,过多的横向动画会影响网页的纵向划屏效率。
2>根据UA信息来判断浏览器是否支持硬件加速(一般使用系统版本来判断,4.0以下的系统不支持硬件加速),如果浏览器不能支持硬件加速,不要使用复杂的3d属性,如persever3D,backface-visibility:hidden,还有overflow:Scroll等硬件加速才能支持比较好的网页效果。应该使用其他更为简单的属性来实现同样的效果。
3> ToDo
网页写法的一个建议; 在一些网站如m.taobao上搜索某类物品后, 结果页面向下拉时会加载得越来越长. 会导致:
1. 使内核生成的picture变得很大, 生成picture耗时, 从而影响UI的更新
2. 可能会使layer变得越来越多, 使texture generator线程和UI线程耗时
这都会造成卡顿. 所以建议做成分页效果