Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
perf(ngAnimate): avoid repeated calls to addClass/removeClass when an…
…imation has no duration

Background:
ngAnimate writes helper classes to DOM elements to see if animations are defined on them. If many
elements have the same definition, and the same parent, we can cache the definition and skip the
application of the helper classes altogether. This helps particularly with large ngRepeat
collections.

Closes angular#14165
Closes angular#14166
Closes angular#16613
  • Loading branch information
matsko authored and Narretz committed Jul 5, 2018
commit 0e2619762398a7cd566c6e7f5141d22081d6b465
1 change: 1 addition & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ var angularFiles = {
'src/ngAnimate/animateJs.js',
'src/ngAnimate/animateJsDriver.js',
'src/ngAnimate/animateQueue.js',
'src/ngAnimate/animateCache.js',
'src/ngAnimate/animation.js',
'src/ngAnimate/ngAnimateSwap.js',
'src/ngAnimate/module.js'
Expand Down
1 change: 1 addition & 0 deletions src/ngAnimate/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
/* ngAnimate directives/services */
"ngAnimateSwapDirective": true,
"$$rAFSchedulerFactory": true,
"$$AnimateCacheProvider": true,
"$$AnimateChildrenDirective": true,
"$$AnimateQueueProvider": true,
"$$AnimationProvider": true,
Expand Down
57 changes: 57 additions & 0 deletions src/ngAnimate/animateCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict';

/** @this */
var $$AnimateCacheProvider = function() {

var KEY = '$$ngAnimateParentKey';
var parentCounter = 0;
var cache = Object.create(null);

this.$get = [function() {
return {
cacheKey: function(node, method, addClass, removeClass) {
var parentNode = node.parentNode;
var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
var parts = [parentID, method, node.getAttribute('class')];
if (addClass) {
parts.push(addClass);
}
if (removeClass) {
parts.push(removeClass);
}
return parts.join(' ');
},

containsCachedAnimationWithoutDuration: function(key) {
var entry = cache[key];

// nothing cached, so go ahead and animate
// otherwise it should be a valid animation
return (entry && !entry.isValid) || false;
},

flush: function() {
cache = Object.create(null);
},

count: function(key) {
var entry = cache[key];
return entry ? entry.total : 0;
},

get: function(key) {
var entry = cache[key];
return entry && entry.value;
},

put: function(key, value, isValid) {
if (!cache[key]) {
cache[key] = { total: 1, value: value, isValid: isValid };
} else {
cache[key].total++;
cache[key].value = value;
}
}
};
}];
};
92 changes: 34 additions & 58 deletions src/ngAnimate/animateCss.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,33 +304,6 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
return [style, value];
}

function createLocalCacheLookup() {
var cache = Object.create(null);
return {
flush: function() {
cache = Object.create(null);
},

count: function(key) {
var entry = cache[key];
return entry ? entry.total : 0;
},

get: function(key) {
var entry = cache[key];
return entry && entry.value;
},

put: function(key, value) {
if (!cache[key]) {
cache[key] = { total: 1, value: value };
} else {
cache[key].total++;
}
}
};
}

// we do not reassign an already present style value since
// if we detect the style property value again we may be
// detecting styles that were added via the `from` styles.
Expand All @@ -349,26 +322,16 @@ function registerRestorableStyles(backup, node, properties) {
}

var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animateProvider) {
var gcsLookup = createLocalCacheLookup();
var gcsStaggerLookup = createLocalCacheLookup();

this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$animateCache',
'$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue',
function($window, $$jqLite, $$AnimateRunner, $timeout,
function($window, $$jqLite, $$AnimateRunner, $timeout, $$animateCache,
$$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) {

var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);

var parentCounter = 0;
function gcsHashFn(node, extraClasses) {
var KEY = '$$ngAnimateParentKey';
var parentNode = node.parentNode;
var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
return parentID + '-' + node.getAttribute('class') + '-' + extraClasses;
}

function computeCachedCssStyles(node, className, cacheKey, properties) {
var timings = gcsLookup.get(cacheKey);
function computeCachedCssStyles(node, className, cacheKey, allowNoDuration, properties) {
var timings = $$animateCache.get(cacheKey);

if (!timings) {
timings = computeCssStyles($window, node, properties);
Expand All @@ -377,20 +340,26 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
}
}

// if a css animation has no duration we
// should mark that so that repeated addClass/removeClass calls are skipped
var hasDuration = allowNoDuration || (timings.transitionDuration > 0 || timings.animationDuration > 0);

// we keep putting this in multiple times even though the value and the cacheKey are the same
// because we're keeping an internal tally of how many duplicate animations are detected.
gcsLookup.put(cacheKey, timings);
$$animateCache.put(cacheKey, timings, hasDuration);

return timings;
}

function computeCachedCssStaggerStyles(node, className, cacheKey, properties) {
var stagger;
var staggerCacheKey = 'stagger-' + cacheKey;

// if we have one or more existing matches of matching elements
// containing the same parent + CSS styles (which is how cacheKey works)
// then staggering is possible
if (gcsLookup.count(cacheKey) > 0) {
stagger = gcsStaggerLookup.get(cacheKey);
if ($$animateCache.count(cacheKey) > 0) {
stagger = $$animateCache.get(staggerCacheKey);

if (!stagger) {
var staggerClassName = pendClasses(className, '-stagger');
Expand All @@ -405,7 +374,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro

$$jqLite.removeClass(node, staggerClassName);

gcsStaggerLookup.put(cacheKey, stagger);
$$animateCache.put(staggerCacheKey, stagger, true);
}
}

Expand All @@ -416,8 +385,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
function waitUntilQuiet(callback) {
rafWaitQueue.push(callback);
$$rAFScheduler.waitUntilQuiet(function() {
gcsLookup.flush();
gcsStaggerLookup.flush();
$$animateCache.flush();

// DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
// PLEASE EXAMINE THE `$$forceReflow` service to understand why.
Expand All @@ -432,8 +400,8 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
});
}

function computeTimings(node, className, cacheKey) {
var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
function computeTimings(node, className, cacheKey, allowNoDuration) {
var timings = computeCachedCssStyles(node, className, cacheKey, allowNoDuration, DETECT_CSS_PROPERTIES);
var aD = timings.animationDelay;
var tD = timings.transitionDelay;
timings.maxDelay = aD && tD
Expand Down Expand Up @@ -520,7 +488,6 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro

var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
var fullClassName = classes + ' ' + preparationClasses;
var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0;

Expand All @@ -533,7 +500,12 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
return closeAndReturnNoopAnimator();
}

var cacheKey, stagger;
var stagger, cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);
if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
preparationClasses = null;
return closeAndReturnNoopAnimator();
}

if (options.stagger > 0) {
var staggerVal = parseFloat(options.stagger);
stagger = {
Expand All @@ -543,7 +515,6 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
animationDuration: 0
};
} else {
cacheKey = gcsHashFn(node, fullClassName);
stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
}

Expand Down Expand Up @@ -577,7 +548,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
var itemIndex = stagger
? options.staggerIndex >= 0
? options.staggerIndex
: gcsLookup.count(cacheKey)
: $$animateCache.count(cacheKey)
: 0;

var isFirst = itemIndex === 0;
Expand All @@ -592,7 +563,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
}

var timings = computeTimings(node, fullClassName, cacheKey);
var timings = computeTimings(node, fullClassName, cacheKey, !isStructural);
var relativeDelay = timings.maxDelay;
maxDelay = Math.max(relativeDelay, 0);
maxDuration = timings.maxDuration;
Expand Down Expand Up @@ -630,6 +601,8 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
return closeAndReturnNoopAnimator();
}

var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);

if (options.delay != null) {
var delayStyle;
if (typeof options.delay !== 'boolean') {
Expand Down Expand Up @@ -717,10 +690,13 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
animationClosed = true;
animationPaused = false;

if (!options.$$skipPreparationClasses) {
if (preparationClasses && !options.$$skipPreparationClasses) {
$$jqLite.removeClass(element, preparationClasses);
}
$$jqLite.removeClass(element, activeClasses);

if (activeClasses) {
$$jqLite.removeClass(element, activeClasses);
}

blockKeyframeAnimations(node, false);
blockTransitions(node, false);
Expand Down Expand Up @@ -904,9 +880,9 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro

if (flags.recalculateTimingStyles) {
fullClassName = node.getAttribute('class') + ' ' + preparationClasses;
cacheKey = gcsHashFn(node, fullClassName);
cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);

timings = computeTimings(node, fullClassName, cacheKey);
timings = computeTimings(node, fullClassName, cacheKey, false);
relativeDelay = timings.maxDelay;
maxDelay = Math.max(relativeDelay, 0);
maxDuration = timings.maxDuration;
Expand Down
2 changes: 1 addition & 1 deletion src/ngAnimate/animateQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
if (existingAnimation.state === RUNNING_STATE) {
normalizeAnimationDetails(element, newAnimation);
} else {
applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
applyGeneratedPreparationClasses($$jqLite, element, isStructural ? event : null, options);

event = newAnimation.event = existingAnimation.event;
options = mergeAnimationDetails(element, existingAnimation, newAnimation);
Expand Down
Loading