Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
fix($animate): perform internal caching on getComputedStyle to boost …
Browse files Browse the repository at this point in the history
…the performance of CSS3 transitions/animations

Closes #4011
Closes #4124
  • Loading branch information
matsko authored and mhevery committed Oct 11, 2013
1 parent 1438f1b commit b1e604e
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 44 deletions.
105 changes: 65 additions & 40 deletions src/ngAnimate/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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';
}

Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
53 changes: 49 additions & 4 deletions test/ngAnimate/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div><span ng-class="klass"></span></div>')($rootScope);
var element = parent.find('span');
Expand Down Expand Up @@ -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('<div></div>')($rootScope);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);

for(var i=0;i<20;i++) {
var kid = $compile('<div class="kid"></div>')($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('<div class="kid c-'+i+'"></div>')($rootScope);
$animate.enter(kid, element);
}

$rootScope.$digest();
$timeout.flush();

expect(count).toBe(40);
});
});
});

0 comments on commit b1e604e

Please sign in to comment.