Skip to content

Commit

Permalink
perf(ngAnimate): avoid repeated calls to addClass/removeClass when an…
Browse files Browse the repository at this point in the history
…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, 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
  • Loading branch information
matsko authored and Narretz committed Jun 25, 2018
1 parent 26a5779 commit ba37426
Show file tree
Hide file tree
Showing 11 changed files with 406 additions and 88 deletions.
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
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(' ');
},

containsCachedAnimationWithDuration: 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;
}
}
};
}];
};
94 changes: 35 additions & 59 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 hasNoDuration = 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, hasNoDuration);

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.containsCachedAnimationWithDuration(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 @@ -903,10 +879,10 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
$$jqLite.addClass(element, activeClasses);

if (flags.recalculateTimingStyles) {
fullClassName = node.getAttribute('class') + ' ' + preparationClasses;
cacheKey = gcsHashFn(node, fullClassName);
fullClassName = node.className + ' ' + preparationClasses;
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

0 comments on commit ba37426

Please sign in to comment.