forked from GoogleChrome/lighthouse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
unsized-images.js
166 lines (145 loc) · 6.96 KB
/
unsized-images.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
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview
* Audit that checks whether all images have explicit width and height.
*/
import {Audit} from './audit.js';
import * as i18n from './../lib/i18n/i18n.js';
import UrlUtils from '../lib/url-utils.js';
const UIStrings = {
/** Title of a Lighthouse audit that provides detail on whether all images have explicit width and height. This descriptive title is shown to users when every image has explicit width and height */
title: 'Image elements have explicit `width` and `height`',
/** Title of a Lighthouse audit that provides detail on whether all images have explicit width and height. This descriptive title is shown to users when one or more images does not have explicit width and height */
failureTitle: 'Image elements do not have explicit `width` and `height`',
/** Description of a Lighthouse audit that tells the user why they should include explicit width and height for all images. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Set an explicit width and height on image elements to reduce layout shifts and improve CLS. [Learn how to set image dimensions](https://web.dev/optimize-cls/#images-without-dimensions)',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class UnsizedImages extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'unsized-images',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
guidanceLevel: 3,
requiredArtifacts: ['ImageElements'],
scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
};
}
/**
* An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
* @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
* @param {string | null} attrValue
* @return {boolean}
*/
static doesHtmlAttrProvideExplicitSize(attrValue) {
if (!attrValue) return false;
// First, superweird edge case of using the positive-sign. The spec _sorta_ says it's valid...
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers
// > Otherwise, if the character is … (+): Advance position to the next character.
// > (The "+" is ignored, but it is not conforming.)
// lol. Irrelevant though b/c Chrome (at least) doesn't ignore. It rejects this as a non-conforming value.
if (attrValue.startsWith('+')) return false;
// parseInt isn't exactly the same as the html's spec for parsing integers, but it's close enough
// https://tc39.es/ecma262/#sec-parseint-string-radix
const int = parseInt(attrValue, 10);
return int >= 0;
}
/**
* An img css size property prevents layout shifts if it is defined, not empty, and not equal to 'auto'.
* @param {string | null} property
* @return {boolean}
*/
static isCssPropExplicitlySet(property) {
if (!property) return false;
return !['auto', 'initial', 'unset', 'inherit'].includes(property);
}
/**
* Images are considered sized if they have defined & valid values.
* @param {LH.Artifacts.ImageElement} image
* @return {boolean}
*/
static isSizedImage(image) {
// Perhaps we hit reachedGatheringBudget before collecting this image's cssWidth/Height
// in fetchSourceRules. In this case, we don't have enough information to determine if it's sized.
// We don't want to show the user a false positive, so we'll call it sized to give it as pass.
// While this situation should only befall small-impact images, it means our analysis is incomplete. :(
// Handwavey TODO: explore ways to avoid this.
if (image.cssEffectiveRules === undefined) return true;
const attrWidth = image.attributeWidth;
const attrHeight = image.attributeHeight;
const cssWidth = image.cssEffectiveRules.width;
const cssHeight = image.cssEffectiveRules.height;
const cssAspectRatio = image.cssEffectiveRules.aspectRatio;
const htmlWidthIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrWidth);
const cssWidthIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssWidth);
const htmlHeightIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrHeight);
const cssHeightIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssHeight);
const explicitAspectRatio = UnsizedImages.isCssPropExplicitlySet(cssAspectRatio);
const explicitWidth = htmlWidthIsExplicit || cssWidthIsExplicit;
const explicitHeight = htmlHeightIsExplicit || cssHeightIsExplicit;
return (explicitWidth && explicitHeight) ||
(explicitWidth && explicitAspectRatio) ||
(explicitHeight && explicitAspectRatio);
}
/**
* @param {LH.Artifacts.ImageElement} image
* @return {boolean}
*/
static isNonNetworkSvg(image) {
const isSvg = UrlUtils.guessMimeType(image.src) === 'image/svg+xml';
const urlScheme = image.src.slice(0, image.src.indexOf(':'));
const isNonNetwork = UrlUtils.isNonNetworkProtocol(urlScheme);
return isSvg && isNonNetwork;
}
/**
* @param {LH.Artifacts} artifacts
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts) {
// CSS background-images & ShadowRoot images are ignored for this audit.
const images = artifacts.ImageElements.filter(el => !el.isCss && !el.isInShadowDOM);
const unsizedImages = [];
for (const image of images) {
// Fixed images are out of document flow and won't cause layout shifts
const isFixedImage =
image.computedStyles.position === 'fixed' || image.computedStyles.position === 'absolute';
if (isFixedImage) continue;
// Non-network SVGs with dimensions don't cause layout shifts in practice, skip them.
// See https://github.com/GoogleChrome/lighthouse/issues/11631
if (UnsizedImages.isNonNetworkSvg(image)) continue;
// The image was sized with HTML or CSS. Good job.
if (UnsizedImages.isSizedImage(image)) continue;
// Images with a 0-size bounding rect (due to hidden parent) aren't part of layout. Cool.
const boundingRect = image.node.boundingRect;
const isNotDisplayed = boundingRect.width === 0 && boundingRect.height === 0;
if (isNotDisplayed) continue;
unsizedImages.push({
url: UrlUtils.elideDataURI(image.src),
node: Audit.makeNodeItem(image.node),
});
}
/** @type {LH.Audit.Details.Table['headings']} */
const headings = [
{key: 'node', valueType: 'node', label: ''},
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
];
return {
score: unsizedImages.length > 0 ? 0 : 1,
details: Audit.makeTableDetails(headings, unsizedImages),
metricSavings: {
CLS: 0,
},
};
}
}
export default UnsizedImages;
export {UIStrings};