02月13, 2017

网页动画性能日志(二)

接上一篇网页动画性能日志(一)

关于js的性能优化

天气模板中所有的动画都是基于canvas的,所以我就只针对canvas来做分析。canvas的兼容性到IE9,所以天气中,凡是IE9以下版本均只展示静态图片。

性能优化一:多画布分层处理动画

由于每一个动画场景中均包含2-4个独立的动画,如:晴天的场景,两个风车的车轮要旋转,太阳发出的光晕要渐隐渐现,这就属于两个完全独立的抽象动画同时播放。

基于以上情况,我们要做的就是将不同动作的动画绘制在不同在canvas画布上面,也就是分层Canvas,动画中的每一层,对渲染和动画的要求是不同的,比如:在游戏里,人物元素的渲染频率就会远远高于背景变化的频率从。在天气场景中,如果将风车和光晕画在同一个画布上面,我们无法单独控制两个动画的播放速度,同时在清除A动画的画布的时候很容易将B动画的绘制清除掉,干扰因素太多; 另一方面,从性能角度来看,HTML5画布元素是基于状态机实施的,没有必要每一层都保持高频的渲染,分层后,能够使得每一层canvas保持不同的重回频率,频率高低均衡,也是对CPU计算压力的一种优化。

分层原理: 用过PS的同学应该都不陌生,canvas的层就相当于PS中的图层,通过z-index来决定层级,最后视觉上展示在屏幕。

alt

如下,绘制出4层canvas画布

<canvas id="mh-scene-rainday"></canvas>
<canvas id="mh-scene-rainday-comp2"></canvas>
<canvas id="mh-scene-rainday-comp1"></canvas>
<canvas id="mh-scene-rainday-comp3"></canvas>

性能优化二:离屏canvas

这一节的关键词:

context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

就像你了解的,drawImage 方法首当其冲是用来绘制图片的(Image 对象),当然它还可以接受Canvas对象,也就是说,可以用它来绘制画布。我们就是利用这个特点,来对canvas进行进一步的优化。

离屏canvas又叫做虚拟canvas,也就是它只作为缓存存在,并不绘制在屏幕上。举个例子:

<canvas  id = "mycanvas"></canvas>
var canvas = document.getElementById(mycanvas);
var ctx = canvas.getContext('2d');
//创建离屏canvas
var offScreenCanvas = document.createElement('canvas');
var offctx = offScreenCanvas.getContext('2d');
offctx.width = 500;
offctx.height = 500;
//所有的计算均放在离屏canvas画布上
offctx.lineTo(10, 10);
offctx.lineWidth = r.width;
offctx.strokeStyle = r.color;
offctx.stroke();

//将离屏画布画到canvas中
ctx.drawImage(offcanvas,0,0,width,height);

为什么要这样处理? context 是一个状态机,你可以改变 context 的若干状态,而几乎所有的渲染操作,最终的效果与 context 本身的状态有关系,所以在canvas中调取API的性能消耗是很大的,尤其是在动画中,需要重复调用,那么这将会成为一笔很大的性能开销。

alt

所以我们将要绘制的内容缓存起来,并将开销大的API调取放在缓存上来调取,甚至包括drawImage本身对图像的裁剪和缩放也放在离屏canvas中执行,对于屏幕展示的canvas我们每一帧的成本就仅仅是展示离屏canvas已经绘制好的图片,这样可以很好的提升性能。

性能优化三:requestAnimationFrame

使用requestAnimationFrame来替代 setTimeout or setInterval,这是一个老生常谈的问题了,简单说一下: requestAnimationFrame 是专门为实现高性能的帧动画而设计的一个API,目前已在多个浏览器得到了支持,包括IE10+,Firefox,Chrome,Safari,Opera等,在移动设备上,ios6以上版本以及IE mobile 10以上也支持requestAnimationFrame,唯一比较遗憾的是目前安卓上的原生浏览器并不支持requestAnimationFrame,不过对requestAnimationFrame的支持应该是大势所趋了,安卓版本的chrome 16+也是支持requestAnimationFrame的。

requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:

  • requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。

  • 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。

  • 你在setTimeout或setInterval中指定的回调函数的执行时机是无法保证的。它将在这一帧动画的某个时间点被执行,很可能是在帧结束的时候。这就意味这我们可能失去这一帧的信息,也就是发生jank。 alt

    具体的数据比较可以参看一下:How do browsers pause/change Javascript when tab or window is not active?

    性能优化四:暂停隐藏canvas

    在web页面中,我们可以将动画抽象为:

    动画 = 计算(GPU or CPU) + 渲染

    对于canvas(采用reauestAnimationFrame)来讲,特点是这样的:

  • 浏览器tab展示,canvas场景动画隐藏:CPU + GPU + 渲染

  • 浏览器tab展示,canvas场景动画展示:CPU + GPU + 渲染

  • 浏览器tab不展示,chrome下none,其他浏览器有可能继续动画

    综上,我们需要分别对第一种情况和第三种情况做优化。 针对第三种情况,我们可以使用PageVisibility,来对document的现实和隐藏进行实时的监听,当浏览器tab 处于hidden状态时,就可以停止动画了(cancelAnimationFrame)。 如下:

           var hidden, visibilityChange; 
                    //兼容处理
            if (typeof document.hidden !== "undefined") {
                        hidden = "hidden";
                        visibilityChange = "visibilitychange";
            } else if (typeof document.msHidden !== "undefined") {
                        hidden = "msHidden";
                        visibilityChange = "msvisibilitychange";
            } else if (typeof document.webkitHidden !== "undefined") {
                        hidden = "webkitHidden";
                        visibilityChange = "webkitvisibilitychange";
            }
            document.addEventListener(visibilityChange,function(){
            if (document[hidden]) {
                /*停止canvas动画*/
                            ...
                }else{
                /*开启canvas动画*/
                            ...
                }
    

    这样页面在最小化或是隐藏状态下动画就完全停止不占用任何资源,当监听页面为展示状态时,再开启动画。这里需要说明的是我们动画中采用了requestAnimationFrame的方式,本来是具有这样的功能的,也就是不需要额外监听pageVisibility,但是不能确保浏览器对requestAnimation优化的处理方式,所有统一加了这样的强制处理。

    现在我们来解决第一种情况,在天气场景中,有可能同时出现3-5个场景,但是只展示一个,其他的均不展示,当canvas场景元素处于隐藏的时候,场景动画并不停止,也就是说它依然占用着内存和资源,所以需要人工干预,当隐藏状态下,强制暂停动画播放。 如下:

    //对IE的兼容
    if(/MSIE/.test(navigator.userAgent) && !window.addEventListener){
        scene.show().siblings(':visible').hide();
    }else{
        scene.siblings(':visible').stop(true,true).fadeOut('slow');
        scene.stop(true,true).fadeIn('slow');
            //要切换场景动画
        if(So.onebox.weather.sceneArr[weatherType]){
                //停止所有的动画
                stopAllAni();
                //播放当前要播放的动画场景
                startAni(So.onebox.weather.sceneArr[weatherType]);
        }
    }
    

    这样,我们页面中有再多的canvas场景,只要处于隐藏状态就不会占用任何资源了。也就是说真正运行的动画只有在用户视野中的场景。

    性能优化五:动画计算中的问题

  • 减少API调用,这个之前提到过

  • 避免使用浮点类型,尽可能使用整型,浮点计算会增加CPU的负担

  • 关于动画实现的算法的优化

    性能优化六:视野之外的绘制

    就像是我们玩游戏一样,整个游戏的场景既大又丰富,如果我们对每一个场景都实时的渲染,那对资源的开销将是巨大的,所以通常情况下,我们只渲染用户视野范围内的场景和用户即将看到的场景,对视野外场景不做处理,这样可以又想到提高资源的利用率,其实上面说的针对多场景停止隐藏场景动画的例子就是这个理念。

    性能优化七:web worker

    使用 Worker 和拆分任务的方法避免复杂算法阻塞动画运行。实现并发运行不同动作的动画,当然这个目前还只是处于尝试阶段。

    关于css的性能优化

    既然谈动画性能,不谈css3动画,那就是不完整的性能日志。

    性能优化八:css3开启GPU加速

    网页动画性能日志(一) 中我曾经提到过: 从性能方面考虑,最理想的渲染流水线是没有布局和绘制环节的,只需要做渲染层的合并即可: alt

    为了实现上述效果,你需要对元素谨慎使用会被修改的样式属性,只能使用那些仅触发渲染层合并的属性。目前,只有两个属性是满足这个条件的:transforms和opacity。

    .moving-element {
      will-change: transform;
    }
    //不支持will-change
    .moving-element {
      transform: translateZ(0);
    }
    

    使用这个CSS属性能提前告知浏览器:这个元素将会执行动画效果。从而浏览器可以提前做一些准备,比如为这个元素创建一个新的渲染层。

    当使用这个属性后,浏览器会被强制开启GPU加速,使得动画更加流畅平滑。我们来看一个对比的例子:

    现在我们用一个简单的高度变化动画来说明为什么具体主线程和合成线程是如何配合工作的原理。

    //transition: height
    div {
        height: 100px;
        transition: height 1s linear;
    }
    
    div:hover {
        height: 200px;
    }
    

    正如你看到的,图中有很多的橘黄色方框,也就是说,浏览器有大量的工作要做,动画可能会出现jank。

    在动画的每一帧中,浏览器都要执行布局、 绘制、 以及将新的位图提交给 GPU。我们知道,将位图加载到 GPU 的内存中是一个相对较慢的操作。

    浏览器需要做大量工作的原因在于每一帧中元素的内容都在不断改变。改变一个元素的高度可能导致需要同步改变它的子元素的大小,所以浏览器必须重新计算布局。布局完成后,主线程又必须重新生成该元素的位图。

    //transition: transform
    div {
        transform: scale(0.5);
        transition: transform 1s linear;
    }
    
    div:hover {
        transform: scale(1.0);
    }
    

    这次我们可以看到少了很多橙色的方框,意味着动画变得更流畅了!那么,为什么执行一个元素的transform动画会跟height动画表现得不一样呢?

    根据定义,CSS 的transform属性不会更改元素或它周围的元素的布局。transform属性会对元素的整体产生影响,它会对整个元素进行缩放、旋转、移动处理。

    这对浏览器来说是个好消息 !浏览器只需要一次生成这个元素的位图,并在动画开始的时候将它提交给GPU去处理 。之后,浏览器不需要再做任何布局、 绘制以及提交位图的操作。从而,浏览器可以充分利用 GPU 的特长去快速地将位图绘制在不同的位置、执行旋转或缩放处理。

    很明显test2要比test1平滑好多。 具体的可以参考:csstrigger

    性能优化九:优化GPU计算

    css3在开启GPU加速后动画平滑了很过,所以我们说css3大法好!那么问题来了,当浏览器tab切换时,不同浏览器对css3动画处理的方式是不一样的,现在再看一下这个公式:

    动画 = 计算(CPU or GPU)+ 渲染

    在safari和firefox中,当前tab隐藏时,计算和渲染均和暂停,当tab处于展示状态时,才会继续播放。 但是在chrome中,当tab处于隐藏状态时,GPU并不会停止计算,但是页面渲染已经暂停了,这是就会出现,当你切换回当前tab时,会出现跳帧的情况。 针对这个问题,目前找到的最好的办法就是:给所有的css3动画dom添加一个同名class,像这样:

    <div class="mh-sunday-sun mh-animation"></div>
    <div class="mh-sunday-bird mh-animation"></div>
    <div class="mh-sunday-wildmillSmall mh-animation"></div>
    <div class="mh-sunday-wildmillBig mh-animation"></div>
    <div class="mh-sunday-wheelSmall mh-animation"></div>
    <div class="mh-sunday-wheelBig mh-animation"></div>
    

    这是为了在js动态干预的时候减少节点遍历的时间,然后利用PageVisibility 当页面隐藏时,将场景中的所有动画元素添加:

    animation-play-state: paused;
    

    属性,来暂停动画,这样在tab隐藏时,就可以有效的暂停GPU的渲染,也在一定程度上,优化了性能。

    关于动画整体优化

    性能优化十:GPU与CPU完美配合

    在写动画的时候,我们一定要分清楚动与静的元素。

  • 所有不需要动的元素尽量避免使用js渲染

  • 具有动画的元素,在写动画的时候要对动画进行有效的抽象,平移,旋转,透明图变化,缩放这样的简单动画,有限使用css3来绘制

  • 复杂动画使用canvas绘制时,要进行分层处理

    优化成果展示

    优化前:

    优化后:

    可以看到,GPU 和 CPU的使用率有明显的下降。

    我们来看一下某一帧的火焰图: 优化前:

    优化后:

    再来对比一下:CPU和GPU的统计数据: 优化前:

    优化后:

    可以看到从动画性能上得到了很大的提升,好了,就这么多啦,更多的还需要去亲自去尝试和摸索~

    关于天气场景所有优化之前和优化折后以及一些抽象demo戳这里:https://github.com/Wineki/WeatherAnimation


    转载请说明出处!

    参考链接:

  • http://taobaofed.org/blog/2016/02/22/canvas-performance/

  • http://www.cnblogs.com/2050/p/3871517.html

  • https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count

  • http://blogs.adobe.com/webplatform/2014/03/18/css-animations-and-transitions-performance/

  • 本文链接:https://www.imwineki.cn/post/WebPerformanceCalendar2.html

    -- EOF --

    Comments

    评论加载中...

    注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。