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

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, 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 #14165
Closes #14166
Closes #16613
  • Loading branch information
matsko authored and Narretz committed Jul 5, 2018
1 parent ed22d2f commit 0936353
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 86 deletions.
1 change: 1 addition & 0 deletions angularFiles.js
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
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
@@ -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
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
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

0 comments on commit 0936353

Please sign in to comment.