资讯专栏INFORMATION COLUMN

求索:GSAP的动画快于jQuery吗?为何?

LiangJ / 2206人阅读

摘要:本文已完结,请看下文求索的动画快于吗为何续本文源自对问题动画性能优于的原理是什么的回答。是这样的吗请看下文求索的动画快于吗为何续

  

本文已完结,请看下文: > 求索:GSAP的动画快于jQuery吗?为何?/续

本文源自对问题《GSAP js动画性能优于jQuery的原理是什么?》的回答。GSAP是一个js动画插件,它声称“20x faster than jQuery”,是什么让它这么快呢?

每当有这样的问题的时候,我们可以通过以下步骤来确定一个未知的解决方案的性能优化是怎么做到/伪造的:

黑盒:从官方用例来看,究竟有多快,快在哪儿

白盒:看看官方用例之内,框架怎么做到优化的

do { 提出假设,自己构建用例测试 } while (假设没有得到验证);

得出结论

文中提到的timer、recalculate、layout、repaint、composite layer,需要浏览器内部运行相关的基础知识。见:

《浏览器的渲染原理简介》

《浏览器的工作原理:新式网络浏览器幕后揭秘》

黑盒:从用例来看,究竟有多快,快在哪儿

首先我们打开chrome,并开启官网的H5动画速度测试页面:http://www.greensock.com/js/speed.html。

页面中用js计算出的fps很不准确,还是以浏览器的统计为准。

在jQuery和GSAP两个框架下打开,然后点run,然后f12审查元素,进入Timeline页面,点record。过了100frame以后暂停,然后进入页面点击stop。

以下是jQuery的结果:100帧6.53s,平均FPS:15帧/秒
(也可以自己算出来 100frames ÷ 6.53s ≈ 15.3FPS

以下是GSAP的结果:100帧2.22s,平均FPS:45帧/秒。比jQuery快2倍呢。

来对比一下100帧里面各个流程的耗时(单位:秒):

类目 详情 jQuery GSAP
scripting timer等js执行 2.87 0.52
rendering recalculate(重计算)、layout(回流) 2.04 0.77
painting repaint(重绘)、composite layers(混合图层) 0.88 0.78
loading 加载 0 0
other stuff 未知 0.06 0.11

我们来看看前3帧里面两个框架都发生了什么:

jQuery:

GSAP:

看来GSAP比起jQuery主要的性能优化在下面这两个类目:

JS:主要是timer,jQuery里面,每帧大概有10~20个timer被触发(并维持在67ms左右);GSAP每帧不超过6个timer(并短于30ms);

渲染:jQuery没有layout步骤,但是GSAP有,而且每次都影响到整个文档;jQuery的recalculate步骤,每次仅影响1个元素;而GSAP每次影响到170左右的元素。

GSAP的渲染详情内容:

jQuery的渲染详情内容:

这样看来,timer造成了很大的区别,而渲染部分本应没有太大区别(layout由于动画部分是position:absolute,影响范围不大),但是二者的最终差异也比较大,我们只有通过源码和用例看到区别了。

白盒:看看用例之内,GSAP框架怎么做到优化的 带着怀疑看看用例

先看看测试页面jQuery和GSAP的用例:

//jQuery
jQuery.easing.cubicIn = $.easing.cubicIn = function( p, n, firstNum, diff ) { //we need to add the standard CubicIn ease to jQuery
   return firstNum + p * p * p * diff;
}
jQuery.fx.interval = 10; //ensures that jQuery refreshes at roughly 100fps like GSAP, TweenJS, and most of the others to be more even/fair.
tests.jquery = {
  milliseconds:true,
  wrapDot:function(dot) {
    return jQuery(dot); //wrap the dot in a jQuery object in order to perform better (that way, we don"t need to query the dom each time we tween - we can just call animate() directly on the jQuery object)
  },
  tween:function(dot) {
    dot[0].style.cssText = startingCSS;
    var angle = Math.random() * Math.PI * 2;
    dot.delay(Math.random() * duration).animate({left:Math.cos(angle) * radius + centerX, 
           top:Math.sin(angle) * radius + centerY, 
           width:32, 
           height:32}, duration, "cubicIn", function() { tests.jquery.tween(dot) });
  },
  stop:function(dot) {
    dot.stop(true);
  },
  nativeSize:false
};

//GSAP (TweenLite) top/left/width/height
tests.gsap = {
  milliseconds:false,
  wrapDot:function(dot) {
    return dot; //no wrapping necessary
  },
  tween:function(dot) {
    var angle = Math.random() * Math.PI * 2;
    dot.style.cssText = startingCSS;
    TweenLite.to(dot, duration, {css:{left:Math.cos(angle) * radius + centerX, 
                      top:Math.sin(angle) * radius + centerY, 
                      width:32, 
                      height:32},
                   delay:Math.random() * duration,
                   ease:Cubic.easeIn,
                   overwrite:"none",
                   onComplete:tests.gsap.tween,
                   onCompleteParams:[dot]});
  },
  stop:function(dot) {
    TweenLite.killTweensOf(dot);
  },
  nativeSize:false
};

function toggleTest() {
   inProgress = !inProgress;
   var i;
   if (inProgress) {
      currentTest = tests[engineInput.value];
      size = (currentTest.nativeSize ? "16px" : "1px");
      centerX = jQuery(window).width() / 2;
      centerY = (jQuery(window).height() / 2) - 30;
      startingCSS = "position:absolute; left:" + centerX + "px; top:"
         + centerY + "px; width:" + size + "; height:" + size + ";"; 
      radius = Math.sqrt(centerX * centerX + centerY * centerY);
      duration = Number(durInput.value);

      createDots();
      i = dots.length;
      while (--i > -1) {
         currentTest.tween(dots[i]);
      }
   }
}

jQuery部分除了时间函数"CubicIn"我们平常用不上以外,其他的部分都符合我们的正常使用习惯。注意到jQuery的jQuery.fx.interval,也就是MsPF(millisecond per frame,我编的单位)被调到了10,换言之,FPS是100。

  

“以让测试更加公平”,注释说。

雪姨:“好大的口气”

让我们接着看源码……

JS运行优化

之前的观测结果表明:JS部分中,主要是timer:jQuery里面,每帧大概有10~20个timer被触发,并维持在67ms左右;GSAP每帧不超过6个timer,同时每帧短于30ms。

我在自己的一个空白页面引用了jQuery的1.10.2的未压缩版,然后用chrome打开页面,并在console输入jQuery.Animation,回车,查看它的定义。并一步步查看其中我觉得有可能会带我到定时器的函数定义,直到得到结果。

在这个过程中,我知道了,jQuery的Animation采用的定时器是setInterval:

jQuery.Animation = function Animation( elem, properties, options ) {
  // 上部分省略...
  jQuery.fx.timer(
    jQuery.extend( tick, {
      elem: elem,
      anim: animation,
      queue: animation.opts.queue
    })
  );

  // attach callbacks from options
  return animation.progress( animation.opts.progress )
    .done( animation.opts.done, animation.opts.complete )
    .fail( animation.opts.fail )
    .always( animation.opts.always );
}

jQuery.fx.timer = function ( timer ) {
   if ( timer() && jQuery.timers.push( timer ) ) {
      jQuery.fx.start();
   }
} 

jQuery.fx.start = function () {
  if ( !timerId ) {
    timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
  }
} 

jQuery.fx.interval = 13;

就算我们不在这个项目里调节jQuery.fx.interval到10,原生的jQuery.fx.interval居然是一13ms/frame,换算成FPS就是77,要知道有一些浏览器的绘制上限是60FPS,即1000ms ÷ 60frame ≈ 16.7 ms/frame,这个interval会要求一些浏览器在绘制上限内执行1.3次,浏览器每隔几帧会丢弃掉其中的1次,而这就造成了额外的损耗,这也是在上面的现象中jQuery里面timer过分耗时,被唤起的次数在20次左右的原因。

而GSAP没有猜错的话,应该是用到requestAnimationFrame(以及低版本IE下的setTimeout作为polyfill),并尽可能剪短定时器内部内容(jQuery处于兼容性考虑,会做大量条件判断,这方面自然会败给GSAP),来压榨定时器性能的。

我们在源码中搜requestAnimationFrame,在TweenLite.js中:

/* Ticker
 */
var _reqAnimFrame = window.requestAnimationFrame, 
_cancelAnimFrame = window.cancelAnimationFrame, 
_getTime = Date.now || function() {return new Date().getTime();},
_lastUpdate = _getTime();

//now try to determine the requestAnimationFrame and cancelAnimationFrame functions and if none are found, we"ll use a setTimeout()/clearTimeout() polyfill.
a = ["ms","moz","webkit","o"];
i = a.length;
while (--i > -1 && !_reqAnimFrame) {
  _reqAnimFrame = window[a[i] + "RequestAnimationFrame"];
  _cancelAnimFrame = window[a[i] + "CancelAnimationFrame"] || window[a[i] + "CancelRequestAnimationFrame"];
}

_class("Ticker", function(fps, useRAF) {
  var _self = this,
    _startTime = _getTime(),
    _useRAF = (useRAF !== false && _reqAnimFrame),
    _fps, _req, _id, _gap, _nextTime,
    _tick = function(manual) {
      _lastUpdate = _getTime();
      _self.time = (_lastUpdate - _startTime) / 1000;
      var overlap = _self.time - _nextTime,
        dispatch;
      if (!_fps || overlap > 0 || manual === true) {
        _self.frame++;
        _nextTime += overlap + (overlap >= _gap ? 0.004 : _gap - overlap);
        dispatch = true;
      }
      if (manual !== true) {
        //make sure the request is made before we dispatch the "tick" event so that
        //timing is maintained.
        //Otherwise, if processing the "tick" requires a bunch of time (like 15ms)
        //and we"re using a setTimeout() that"s based on 16.7ms,
        //it"d technically take 31.7ms between frames otherwise.
        _id = _req(_tick);
      }
      if (dispatch) {
        _self.dispatchEvent("tick");
      }
    };
  // ...
  _self.wake = function() {
    if (_id !== null) {
      _self.sleep();
    }
    _req = (_fps === 0) ? _emptyFunc : (!_useRAF || !_reqAnimFrame) ? function(f) { return setTimeout(f, ((_nextTime - _self.time) * 1000 + 1) | 0); } : _reqAnimFrame;
    if (_self === _ticker) {
      _tickerActive = true;
    }
    _tick(2);
  };
}

这段代码是非常典型的requestAnimationFrame的polyfill。并且在polyfill部分,计算了浏览器的绘制上限的时间间隔,也符合我之前的猜测。

渲染优化

之前的观测结果表明,渲染部分,jQuery没有layout步骤,但是GSAP有,而且每次都影响到整个文档;jQuery的recalculate步骤,每次仅影响1个元素;而GSAP每次影响到170左右的元素。

从观测结果来看,GSAP做的是化零为整,一次性重新布局全部元素的活儿(一次性改变它们的top、left值,甚至有可能是重新替换了一整个DOM内部的全部HTML)。

是这样吗?GSAP的源码太过庞大,我们怎么构造对代码结构的感性认识呢?

我把官方用例保存到了本地,用的是xxx.htm,这样会生成一个xxx_files文件夹,里面有所有引用的资源文件。(很有意思,png没有保存下来)。然后我用没有压缩过的源代码文件替代了TweenLite.min.jsCSSPlugin.min.js

现在我再生成一次timeline:

这个时候触发Recalculate style的代码行数与调用栈非常清晰了。我点进p.setRatio@CSSPlugin.min.js:2066,在当前行新建一个断点,然后刷新页面:

调用栈与上下文都出现了,这个时候的源码是清晰可读的。上下文是没有innerHTML或者$.html之类的代码,我在这里知道,没有采用一次性刷新innerHTML的方法(事实上,这样做的代价也很高)。这里每一步改变的都是CSSStyleDeclaration,但这个CSSStyleDeclaration没有连接到相应元素。

仔细阅读调用栈每一层的上下文之后,我做出了它分层的依据:

调用栈(自顶向下) 代码理解
_tick 重绘时间管理层,用rAF函数绘制一帧
EventDispatcher.dispatchEvent 事件处理层,用事件代替回调
Animation._updateRoot 动画管理层,在这里还会每隔一定帧数做一次gc
SimpleTimeline.render 时间线管理层,链式帧的结构
TweenLite.render 帧管理层,本帧和下一帧的引用,计算Tween值
CSSPlugin.setRatio CSS样式管理层,处理CSS样式最终的格式

对jQuery的测试用例做同样的事情,可以看到,在最底端,也就是触发recalculate的一端,style直接引用的是DOM元素的style。

jQuery.extend.style = function( elem, name, value, extra ) {
  var ret, type, hooks,
    origName = jQuery.camelCase( name ),
    style = elem.style;
  name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
  hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];

  if ( value !== undefined ) {
    //style里面的一大堆判断省略
    style[ name ] = value;
  }
}

画出相应的架构:

调用栈(自顶向下) 代码理解
jQuery.dequeue.next jQuery队列函数
jQuery.fn.animate Animate函数,这里建立Animation对象
jQuery.fx.timer 定时器管理,在这里缓存所有的定时器
jQuery.fx.start 使用setInterval开始一个定时器
Animation.tick Animation对象,管理帧和tween值(中间值)的关系
jQuery.Tween.run Tween对象,处理中间值和时间函数的关系
jQuery.Tween.propHooks.set 抽象set函数,以set各种prop
jQuery.style set函数的实例化,处理元素的style

意识到了吗,jQuery是过程化的,每个函数/类代表一个需要管理/控制兼容性的需求。

假设与验证,自己构建测试用例,看看优化是否名不副实

综上所述,我们得到以下假设:

jQuery的定时器采用的是setInterval,受到浏览器重绘上限的控制,而GSAP采用requestAnimationFrame,完全将重绘交给浏览器管理,以获得更好地重绘性能

jQuery每次都是多带带修改一个DOM的style,而GSAP是计算离线的style,然后再赋给DOM。这导致了不一样的时间开销。

jQuery没有集中绘制,每个DOM都在一个事件回调函数上下文中处理,有多少个DOM就有多少个上下文;GSAP有集中绘制。同时jQuery是过程化的,GSAP是面向对象的。这让jQuery非常难以做到集中控制绘制。jQuery会将DOM的引用一路传递到最终改变DOM的style函数中,这在调用过程中也会非常浪费空间。这些也都导致了不一样的时间开销。

是这样的吗?

请看下文: 求索:GSAP的动画快于jQuery吗?为何?/续

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/110843.html

相关文章

  • 求索GSAP动画快于jQuery?/ 续 V1.1

    摘要:本文是求索的动画快于吗为何的续文。没有集中绘制,每个都在一个事件回调函数上下文中处理,有多少个就有多少个上下文有集中绘制。测试过程中为了比较好的效果用了随机数。 本文是求索:GSAP的动画快于jQuery吗?为何? 的续文。GSAP是一个js动画插件,它声称20x faster than jQuery,是什么让它这么快呢? 每当有这样的问题的时候,我们可以通过以下步骤来...

    Tecode 评论0 收藏0
  • requestAnimationFrame Web中写动画另一种选择

    摘要:现在又多了一种实现动画的方案,那就是还在草案当中的方法。这个方法就是传递给的回调函数。为回调函数一个简单的例子模拟一个进度条动画,初始宽度为在函数中将进度加然后再更新到宽度上,在进度达到之前,一直重复这一过程。 HTML5/CSS3时代,我们要在web里做动画选择其实已经很多了: 你可以用CSS3的animattion+keyframes; 你也可以用css3的transition...

    Prasanta 评论0 收藏0
  • requestAnimationFrame Web中写动画另一种选择

    摘要:现在又多了一种实现动画的方案,那就是还在草案当中的方法。这个方法就是传递给的回调函数。为回调函数一个简单的例子模拟一个进度条动画,初始宽度为在函数中将进度加然后再更新到宽度上,在进度达到之前,一直重复这一过程。 HTML5/CSS3时代,我们要在web里做动画选择其实已经很多了: 你可以用CSS3的animattion+keyframes; 你也可以用css3的transition...

    alin 评论0 收藏0
  • requestAnimationFrame Web中写动画另一种选择

    摘要:现在又多了一种实现动画的方案,那就是还在草案当中的方法。这个方法就是传递给的回调函数。为回调函数一个简单的例子模拟一个进度条动画,初始宽度为在函数中将进度加然后再更新到宽度上,在进度达到之前,一直重复这一过程。 HTML5/CSS3时代,我们要在web里做动画选择其实已经很多了: 你可以用CSS3的animattion+keyframes; 你也可以用css3的transition...

    piapia 评论0 收藏0
  • GSAP - 专业 Web 动画

    摘要:虽然没有视觉效果,但这就是基本的值动画。有专门的位置可以查询缓动函数。另外,不要期望在不支持的浏览器上做动画。是专业动画库,在大部分情况下,它也具备更好的动画性能。 说到在网页里创建动画,你可能很快会想到jQuery的animate()方法,或者css3的animation和transition。现在,本文将介绍另一个web动画的可选方案,GSAP。 GSAP的全名是GreenSock...

    MASAILA 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<