From ddcbf7604af44e264aeeed75f1afb4a477a637fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 9 Oct 2013 01:12:03 -0400 Subject: [PATCH] fix($animate): perform internal caching on getComputedStyle to boost the performance of CSS3 transitions/animations Closes #4011 Closes #4124 --- src/ngAnimate/animate.js | 105 +++++++++++++++++++++------------- test/ngAnimate/animateSpec.js | 53 +++++++++++++++-- 2 files changed, 114 insertions(+), 44 deletions(-) diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 4bd7b886d817..f5be6b766cb0 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -642,6 +642,10 @@ angular.module('ngAnimate', ['ng']) animationIterationCountKey = 'IterationCount', ELEMENT_NODE = 1; + var NG_ANIMATE_PARENT_KEY = '$ngAnimateKey'; + var lookupCache = {}; + var parentCounter = 0; + var animationReflowQueue = [], animationTimer, timeOut = false; function afterReflow(callback) { animationReflowQueue.push(callback); @@ -652,65 +656,93 @@ angular.module('ngAnimate', ['ng']) }); animationReflowQueue = []; animationTimer = null; + lookupCache = {}; }, 10, false); } - function animate(element, className, done) { - if(['ng-enter','ng-leave','ng-move'].indexOf(className) == -1) { - var existingDuration = 0; + function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) { + var data = lookupCache[cacheKey]; + if(!data) { + var transitionDuration = 0, transitionDelay = 0, + animationDuration = 0, animationDelay = 0; + + //we want all the styles defined before and after forEach(element, function(element) { if (element.nodeType == ELEMENT_NODE) { var elementStyles = $window.getComputedStyle(element) || {}; - existingDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), - existingDuration); + + transitionDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), transitionDuration); + + if(!onlyCheckTransition) { + transitionDelay = Math.max(parseMaxTime(elementStyles[transitionProp + delayKey]), transitionDelay); + + animationDelay = Math.max(parseMaxTime(elementStyles[animationProp + delayKey]), animationDelay); + + var aDuration = parseMaxTime(elementStyles[animationProp + durationKey]); + + if(aDuration > 0) { + aDuration *= parseInt(elementStyles[animationProp + animationIterationCountKey]) || 1; + } + + animationDuration = Math.max(aDuration, animationDuration); + } } }); - if(existingDuration > 0) { - done(); - return; - } + data = { + transitionDelay : transitionDelay, + animationDelay : animationDelay, + transitionDuration : transitionDuration, + animationDuration : animationDuration + }; + lookupCache[cacheKey] = data; } + return data; + } - element.addClass(className); - - //we want all the styles defined before and after - var transitionDuration = 0, - animationDuration = 0, - transitionDelay = 0, - animationDelay = 0; - forEach(element, function(element) { - if (element.nodeType == ELEMENT_NODE) { - var elementStyles = $window.getComputedStyle(element) || {}; + function parseMaxTime(str) { + var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; + forEach(values, function(value) { + total = Math.max(parseFloat(value) || 0, total); + }); + return total; + } - transitionDelay = Math.max(parseMaxTime(elementStyles[transitionProp + delayKey]), transitionDelay); + function getCacheKey(element) { + var parent = element.parent(); + var parentID = parent.data(NG_ANIMATE_PARENT_KEY); + if(!parentID) { + parent.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); + parentID = parentCounter; + } + return parentID + '-' + element[0].className; + } - animationDelay = Math.max(parseMaxTime(elementStyles[animationProp + delayKey]), animationDelay); + function animate(element, className, done) { - transitionDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), transitionDuration); + var cacheKey = getCacheKey(element); + if(getElementAnimationDetails(element, cacheKey, true).transitionDuration > 0) { - var aDuration = parseMaxTime(elementStyles[animationProp + durationKey]); + done(); + return; + } - if(aDuration > 0) { - aDuration *= parseInt(elementStyles[animationProp + animationIterationCountKey]) || 1; - } + element.addClass(className); - animationDuration = Math.max(aDuration, animationDuration); - } - }); + var timings = getElementAnimationDetails(element, cacheKey + ' ' + className); /* there is no point in performing a reflow if the animation timeout is empty (this would cause a flicker bug normally in the page. There is also no point in performing an animation that only has a delay and no duration */ - var maxDuration = Math.max(transitionDuration, animationDuration); + var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); if(maxDuration > 0) { - var maxDelayTime = Math.max(transitionDelay, animationDelay) * 1000, + var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * 1000, startTime = Date.now(), node = element[0]; //temporarily disable the transition so that the enter styles //don't animate twice (this is here to avoid a bug in Chrome/FF). - if(transitionDuration > 0) { + if(timings.transitionDuration > 0) { node.style[transitionProp + propertyKey] = 'none'; } @@ -723,7 +755,7 @@ angular.module('ngAnimate', ['ng']) var css3AnimationEvents = animationendEvent + ' ' + transitionendEvent; afterReflow(function() { - if(transitionDuration > 0) { + if(timings.transitionDuration > 0) { node.style[transitionProp + propertyKey] = ''; } element.addClass(activeClassName); @@ -768,13 +800,6 @@ angular.module('ngAnimate', ['ng']) } } - function parseMaxTime(str) { - var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; - forEach(values, function(value) { - total = Math.max(parseFloat(value) || 0, total); - }); - return total; - } } return { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 3652e450f832..cae252665a0e 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -726,10 +726,10 @@ describe("ngAnimate", function() { it('should re-evaluate the CSS classes for an animation each time', inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout, $compile) { - ss.addRule('.abc', '-webkit-transition:22s linear all;' + - 'transition:22s linear all;'); - ss.addRule('.xyz', '-webkit-transition:11s linear all;' + - 'transition:11s linear all;'); + ss.addRule('.abc.ng-enter', '-webkit-transition:22s linear all;' + + 'transition:22s linear all;'); + ss.addRule('.xyz.ng-enter', '-webkit-transition:11s linear all;' + + 'transition:11s linear all;'); var parent = $compile('
')($rootScope); var element = parent.find('span'); @@ -1875,4 +1875,49 @@ describe("ngAnimate", function() { expect(intercepted).toBe(true); }); }); + + it("should cache the response from getComputedStyle if each successive element has the same className value and parent until the first reflow hits", function() { + var count = 0; + module(function($provide) { + $provide.value('$window', { + document : jqLite(window.document), + getComputedStyle: function(element) { + count++; + return window.getComputedStyle(element); + } + }); + }); + + inject(function($animate, $rootScope, $compile, $rootElement, $timeout, $document, $sniffer) { + if(!$sniffer.transitions) return; + + $animate.enabled(true); + + var element = $compile('
')($rootScope); + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + for(var i=0;i<20;i++) { + var kid = $compile('
')($rootScope); + $animate.enter(kid, element); + } + $rootScope.$digest(); + $timeout.flush(); + + expect(count).toBe(2); + + dealoc(element); + count = 0; + + for(var i=0;i<20;i++) { + var kid = $compile('
')($rootScope); + $animate.enter(kid, element); + } + + $rootScope.$digest(); + $timeout.flush(); + + expect(count).toBe(40); + }); + }); });