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

Commit 0e26197

Browse files
matskoNarretz
authored andcommitted
perf(ngAnimate): avoid repeated calls to addClass/removeClass when animation 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
1 parent 74726b4 commit 0e26197

12 files changed

+408
-86
lines changed

angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ var angularFiles = {
104104
'src/ngAnimate/animateJs.js',
105105
'src/ngAnimate/animateJsDriver.js',
106106
'src/ngAnimate/animateQueue.js',
107+
'src/ngAnimate/animateCache.js',
107108
'src/ngAnimate/animation.js',
108109
'src/ngAnimate/ngAnimateSwap.js',
109110
'src/ngAnimate/module.js'

src/ngAnimate/.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
/* ngAnimate directives/services */
7474
"ngAnimateSwapDirective": true,
7575
"$$rAFSchedulerFactory": true,
76+
"$$AnimateCacheProvider": true,
7677
"$$AnimateChildrenDirective": true,
7778
"$$AnimateQueueProvider": true,
7879
"$$AnimationProvider": true,

src/ngAnimate/animateCache.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
/** @this */
4+
var $$AnimateCacheProvider = function() {
5+
6+
var KEY = '$$ngAnimateParentKey';
7+
var parentCounter = 0;
8+
var cache = Object.create(null);
9+
10+
this.$get = [function() {
11+
return {
12+
cacheKey: function(node, method, addClass, removeClass) {
13+
var parentNode = node.parentNode;
14+
var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
15+
var parts = [parentID, method, node.getAttribute('class')];
16+
if (addClass) {
17+
parts.push(addClass);
18+
}
19+
if (removeClass) {
20+
parts.push(removeClass);
21+
}
22+
return parts.join(' ');
23+
},
24+
25+
containsCachedAnimationWithoutDuration: function(key) {
26+
var entry = cache[key];
27+
28+
// nothing cached, so go ahead and animate
29+
// otherwise it should be a valid animation
30+
return (entry && !entry.isValid) || false;
31+
},
32+
33+
flush: function() {
34+
cache = Object.create(null);
35+
},
36+
37+
count: function(key) {
38+
var entry = cache[key];
39+
return entry ? entry.total : 0;
40+
},
41+
42+
get: function(key) {
43+
var entry = cache[key];
44+
return entry && entry.value;
45+
},
46+
47+
put: function(key, value, isValid) {
48+
if (!cache[key]) {
49+
cache[key] = { total: 1, value: value, isValid: isValid };
50+
} else {
51+
cache[key].total++;
52+
cache[key].value = value;
53+
}
54+
}
55+
};
56+
}];
57+
};

src/ngAnimate/animateCss.js

+34-58
Original file line numberDiff line numberDiff line change
@@ -304,33 +304,6 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
304304
return [style, value];
305305
}
306306

307-
function createLocalCacheLookup() {
308-
var cache = Object.create(null);
309-
return {
310-
flush: function() {
311-
cache = Object.create(null);
312-
},
313-
314-
count: function(key) {
315-
var entry = cache[key];
316-
return entry ? entry.total : 0;
317-
},
318-
319-
get: function(key) {
320-
var entry = cache[key];
321-
return entry && entry.value;
322-
},
323-
324-
put: function(key, value) {
325-
if (!cache[key]) {
326-
cache[key] = { total: 1, value: value };
327-
} else {
328-
cache[key].total++;
329-
}
330-
}
331-
};
332-
}
333-
334307
// we do not reassign an already present style value since
335308
// if we detect the style property value again we may be
336309
// detecting styles that were added via the `from` styles.
@@ -349,26 +322,16 @@ function registerRestorableStyles(backup, node, properties) {
349322
}
350323

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

355-
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
326+
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$animateCache',
356327
'$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue',
357-
function($window, $$jqLite, $$AnimateRunner, $timeout,
328+
function($window, $$jqLite, $$AnimateRunner, $timeout, $$animateCache,
358329
$$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) {
359330

360331
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
361332

362-
var parentCounter = 0;
363-
function gcsHashFn(node, extraClasses) {
364-
var KEY = '$$ngAnimateParentKey';
365-
var parentNode = node.parentNode;
366-
var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
367-
return parentID + '-' + node.getAttribute('class') + '-' + extraClasses;
368-
}
369-
370-
function computeCachedCssStyles(node, className, cacheKey, properties) {
371-
var timings = gcsLookup.get(cacheKey);
333+
function computeCachedCssStyles(node, className, cacheKey, allowNoDuration, properties) {
334+
var timings = $$animateCache.get(cacheKey);
372335

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

343+
// if a css animation has no duration we
344+
// should mark that so that repeated addClass/removeClass calls are skipped
345+
var hasDuration = allowNoDuration || (timings.transitionDuration > 0 || timings.animationDuration > 0);
346+
380347
// we keep putting this in multiple times even though the value and the cacheKey are the same
381348
// because we're keeping an internal tally of how many duplicate animations are detected.
382-
gcsLookup.put(cacheKey, timings);
349+
$$animateCache.put(cacheKey, timings, hasDuration);
350+
383351
return timings;
384352
}
385353

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

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

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

406375
$$jqLite.removeClass(node, staggerClassName);
407376

408-
gcsStaggerLookup.put(cacheKey, stagger);
377+
$$animateCache.put(staggerCacheKey, stagger, true);
409378
}
410379
}
411380

@@ -416,8 +385,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
416385
function waitUntilQuiet(callback) {
417386
rafWaitQueue.push(callback);
418387
$$rAFScheduler.waitUntilQuiet(function() {
419-
gcsLookup.flush();
420-
gcsStaggerLookup.flush();
388+
$$animateCache.flush();
421389

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

435-
function computeTimings(node, className, cacheKey) {
436-
var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
403+
function computeTimings(node, className, cacheKey, allowNoDuration) {
404+
var timings = computeCachedCssStyles(node, className, cacheKey, allowNoDuration, DETECT_CSS_PROPERTIES);
437405
var aD = timings.animationDelay;
438406
var tD = timings.transitionDelay;
439407
timings.maxDelay = aD && tD
@@ -520,7 +488,6 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
520488

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

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

536-
var cacheKey, stagger;
503+
var stagger, cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);
504+
if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
505+
preparationClasses = null;
506+
return closeAndReturnNoopAnimator();
507+
}
508+
537509
if (options.stagger > 0) {
538510
var staggerVal = parseFloat(options.stagger);
539511
stagger = {
@@ -543,7 +515,6 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
543515
animationDuration: 0
544516
};
545517
} else {
546-
cacheKey = gcsHashFn(node, fullClassName);
547518
stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
548519
}
549520

@@ -577,7 +548,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
577548
var itemIndex = stagger
578549
? options.staggerIndex >= 0
579550
? options.staggerIndex
580-
: gcsLookup.count(cacheKey)
551+
: $$animateCache.count(cacheKey)
581552
: 0;
582553

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

595-
var timings = computeTimings(node, fullClassName, cacheKey);
566+
var timings = computeTimings(node, fullClassName, cacheKey, !isStructural);
596567
var relativeDelay = timings.maxDelay;
597568
maxDelay = Math.max(relativeDelay, 0);
598569
maxDuration = timings.maxDuration;
@@ -630,6 +601,8 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
630601
return closeAndReturnNoopAnimator();
631602
}
632603

604+
var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
605+
633606
if (options.delay != null) {
634607
var delayStyle;
635608
if (typeof options.delay !== 'boolean') {
@@ -717,10 +690,13 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
717690
animationClosed = true;
718691
animationPaused = false;
719692

720-
if (!options.$$skipPreparationClasses) {
693+
if (preparationClasses && !options.$$skipPreparationClasses) {
721694
$$jqLite.removeClass(element, preparationClasses);
722695
}
723-
$$jqLite.removeClass(element, activeClasses);
696+
697+
if (activeClasses) {
698+
$$jqLite.removeClass(element, activeClasses);
699+
}
724700

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

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

909-
timings = computeTimings(node, fullClassName, cacheKey);
885+
timings = computeTimings(node, fullClassName, cacheKey, false);
910886
relativeDelay = timings.maxDelay;
911887
maxDelay = Math.max(relativeDelay, 0);
912888
maxDuration = timings.maxDuration;

src/ngAnimate/animateQueue.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
438438
if (existingAnimation.state === RUNNING_STATE) {
439439
normalizeAnimationDetails(element, newAnimation);
440440
} else {
441-
applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
441+
applyGeneratedPreparationClasses($$jqLite, element, isStructural ? event : null, options);
442442

443443
event = newAnimation.event = existingAnimation.event;
444444
options = mergeAnimationDetails(element, existingAnimation, newAnimation);

0 commit comments

Comments
 (0)