forked from checkinholiday/lighthouse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnon-composited-animations.js
213 lines (195 loc) · 8.28 KB
/
non-composited-animations.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Audit which reports all animations that failed to composite along
* with the failure reasons. Failure reasons are only reported if they are actionable.
* https://docs.google.com/document/d/1XKcJP2CKmNKfOcDsVvliAQ-e1H9C1nf2H-pzTdyafAA/edit?usp=sharing
*/
import {Audit} from './audit.js';
import * as i18n from '../lib/i18n/i18n.js';
const UIStrings = {
/** Title of a diagnostic LH audit that provides details on animations that are not composited. */
title: 'Avoid non-composited animations',
/** Description of a diagnostic LH audit that shows the user animations that are not composited. Janky means frames may be skipped and the animation will look bad. Acceptable alternatives here might be 'poor', or 'slow'. */
description: 'Animations which are not composited can be janky and increase CLS. ' +
'[Learn how to avoid non-composited animations](https://developer.chrome.com/docs/lighthouse/performance/non-composited-animations/)',
/** [ICU Syntax] Label identifying the number of animated elements that are not composited. */
displayValue: `{itemCount, plural,
=1 {# animated element found}
other {# animated elements found}
}`,
/**
* @description [ICU Syntax] Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the animated CSS property not being supported on the compositor. Shown in a table with a list of other potential failure reasons.
* @example {height, width} properties
*/
unsupportedCSSProperty: `{propertyCount, plural,
=1 {Unsupported CSS Property: {properties}}
other {Unsupported CSS Properties: {properties}}
}`,
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to a `transform` property being dependent on the size of the element itself. Shown in a table with a list of other potential failure reasons. */
transformDependsBoxSize: 'Transform-related property depends on box size',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to a `filter` property possibly moving pixels. Shown in a table with a list of other potential failure reasons. */
filterMayMovePixels: 'Filter-related property may move pixels',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to an effect having a composite mode which is not `replace`. Shown in a table with a list of other potential failure reasons. */
nonReplaceCompositeMode: 'Effect has composite mode other than "replace"',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to another animation on the same target being incompatible. Shown in a table with a list of other potential failure reasons. */
incompatibleAnimations: 'Target has another animation which is incompatible',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to an effect having unsupported timing parameters. Shown in a table with a list of other potential failure reasons. */
unsupportedTimingParameters: 'Effect has unsupported timing parameters',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
/**
* Each failure reason is represented by a bit flag. The bit shift operator '<<' is used to define which bit corresponds to each failure reason.
* https://source.chromium.org/search?q=f:compositor_animations.h%20%22enum%20FailureReason%22
* @type {{flag: number, text: string}[]}
*/
const ACTIONABLE_FAILURE_REASONS = [
{
flag: 1 << 13,
text: UIStrings.unsupportedCSSProperty,
},
{
flag: 1 << 11,
text: UIStrings.transformDependsBoxSize,
},
{
flag: 1 << 12,
text: UIStrings.filterMayMovePixels,
},
{
flag: 1 << 4,
text: UIStrings.nonReplaceCompositeMode,
},
{
flag: 1 << 6,
text: UIStrings.incompatibleAnimations,
},
{
flag: 1 << 3,
text: UIStrings.unsupportedTimingParameters,
},
];
/**
* Return list of actionable failure reasons and a boolean if some reasons are not actionable.
* Each flag is a number with a single bit set to 1 in the position corresponding to a failure reason.
* We can check if a specific bit is true in the failure coding using bitwise and '&' with the flag.
* @param {number} failureCode
* @param {string[]} unsupportedProperties
* @return {LH.IcuMessage[]}
*/
function getActionableFailureReasons(failureCode, unsupportedProperties) {
return ACTIONABLE_FAILURE_REASONS
.filter(reason => failureCode & reason.flag)
.map(reason => {
if (reason.text === UIStrings.unsupportedCSSProperty) {
return str_(reason.text, {
propertyCount: unsupportedProperties.length,
properties: unsupportedProperties.join(', '),
});
}
return str_(reason.text);
});
}
class NonCompositedAnimations extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'non-composited-animations',
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
title: str_(UIStrings.title),
description: str_(UIStrings.description),
guidanceLevel: 2,
requiredArtifacts: ['TraceElements', 'HostUserAgent'],
};
}
/**
* @param {LH.Artifacts} artifacts
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts) {
// COMPAT: This audit requires m86
const match = artifacts.HostUserAgent.match(/Chrome\/(\d+)/);
if (!match || Number(match[1]) < 86) {
return {
score: 1,
notApplicable: true,
metricSavings: {CLS: 0},
};
}
/** @type {LH.Audit.Details.TableItem[]} */
const results = [];
let shouldAddAnimationNameColumn = false;
artifacts.TraceElements.forEach(element => {
if (element.traceEventType !== 'animation') return;
const animations = element.animations || [];
const animationReasons = new Map();
for (const {name, failureReasonsMask, unsupportedProperties} of animations) {
if (!failureReasonsMask) continue;
const failureReasons =
getActionableFailureReasons(failureReasonsMask, unsupportedProperties || []);
for (const failureReason of failureReasons) {
if (name) {
shouldAddAnimationNameColumn = true;
}
const reasons = animationReasons.get(name) || new Set();
reasons.add(failureReason);
animationReasons.set(name, reasons);
}
}
if (!animationReasons.size) return;
const allFailureReasons = [];
for (const [name, reasons] of animationReasons) {
for (const failureReason of reasons) {
allFailureReasons.push({
failureReason,
animation: name,
});
}
}
results.push({
node: Audit.makeNodeItem(element.node),
subItems: {
type: 'subitems',
items: allFailureReasons,
},
});
});
/** @type {LH.Audit.Details.Table['headings']} */
const headings = [
/* eslint-disable max-len */
{key: 'node', valueType: 'node', subItemsHeading: {key: 'failureReason', valueType: 'text'}, label: str_(i18n.UIStrings.columnElement)},
/* eslint-enable max-len */
];
if (shouldAddAnimationNameColumn) {
headings.push(
/* eslint-disable max-len */
{key: null, valueType: 'text', subItemsHeading: {key: 'animation', valueType: 'text'}, label: str_(i18n.UIStrings.columnName)}
/* eslint-enable max-len */
);
}
const details = Audit.makeTableDetails(headings, results);
let displayValue;
if (results.length > 0) {
displayValue = str_(UIStrings.displayValue, {itemCount: results.length});
}
return {
score: results.length === 0 ? 1 : 0,
notApplicable: results.length === 0,
metricSavings: {
// We do not have enough information to accurately predict the impact of individual animations on CLS.
// It is also not worth the effort since only a small percentage of sites have their CLS affected by non-composited animations.
// https://github.com/GoogleChrome/lighthouse/pull/15099#issuecomment-1558107906
CLS: 0,
},
details,
displayValue,
};
}
}
export default NonCompositedAnimations;
export {UIStrings};