-
Notifications
You must be signed in to change notification settings - Fork 9.3k
/
modern-image-formats.js
187 lines (162 loc) · 7.52 KB
/
modern-image-formats.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
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/*
* @fileoverview This audit determines if the images could be smaller when compressed with WebP.
*/
import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
import UrlUtils from '../../lib/url-utils.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Imperative title of a Lighthouse audit that tells the user to serve images in newer and more efficient image formats in order to enhance the performance of a page. A non-modern image format was designed 20+ years ago. This is displayed in a list of audit titles that Lighthouse generates. */
title: 'Serve images in next-gen formats',
/** Description of a Lighthouse audit that tells the user *why* they should use newer and more efficient image formats. 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: 'Image formats like WebP and AVIF often provide better ' +
'compression than PNG or JPEG, which means faster downloads and less data consumption. ' +
'[Learn more about modern image formats](https://developer.chrome.com/docs/lighthouse/performance/uses-webp-images/).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
const IGNORE_THRESHOLD_IN_BYTES = 8192;
class ModernImageFormats extends ByteEfficiencyAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'modern-image-formats',
title: str_(UIStrings.title),
description: str_(UIStrings.description),
scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.METRIC_SAVINGS,
guidanceLevel: 3,
requiredArtifacts: ['OptimizedImages', 'devtoolsLogs', 'traces', 'URL', 'GatherContext',
'ImageElements'],
};
}
/**
* @param {{naturalWidth: number, naturalHeight: number}} imageElement
* @return {number}
*/
static estimateWebPSizeFromDimensions(imageElement) {
const totalPixels = imageElement.naturalWidth * imageElement.naturalHeight;
// See uses-optimized-images for the rationale behind our 2 byte-per-pixel baseline and
// JPEG compression ratio of 8:1.
// WebP usually gives ~20% additional savings on top of that, so we will use 10:1.
// This is quite pessimistic as their study shows a photographic compression ratio of ~29:1.
// https://developers.google.com/speed/webp/docs/webp_lossless_alpha_study#results
const expectedBytesPerPixel = 2 * 1 / 10;
return Math.round(totalPixels * expectedBytesPerPixel);
}
/**
* @param {{naturalWidth: number, naturalHeight: number}} imageElement
* @return {number}
*/
static estimateAvifSizeFromDimensions(imageElement) {
const totalPixels = imageElement.naturalWidth * imageElement.naturalHeight;
// See above for the rationale behind our 2 byte-per-pixel baseline and WebP ratio of 10:1.
// AVIF usually gives ~20% additional savings on top of that, so we will use 12:1.
// This is quite pessimistic as Netflix study shows a photographic compression ratio of ~40:1
// (0.4 *bits* per pixel at SSIM 0.97).
// https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4
const expectedBytesPerPixel = 2 * 1 / 12;
return Math.round(totalPixels * expectedBytesPerPixel);
}
/**
* @param {{jpegSize: number | undefined, webpSize: number | undefined}} otherFormatSizes
* @return {number|undefined}
*/
static estimateAvifSizeFromWebPAndJpegEstimates(otherFormatSizes) {
if (!otherFormatSizes.jpegSize || !otherFormatSizes.webpSize) return undefined;
// AVIF saves at least ~50% on JPEG, ~20% on WebP at low quality.
// http://downloads.aomedia.org/assets/pdf/symposium-2019/slides/CyrilConcolato_Netflix-AVIF-AOM-Research-Symposium-2019.pdf
// https://jakearchibald.com/2020/avif-has-landed/
// https://www.finally.agency/blog/what-is-avif-image-format
// See https://github.com/GoogleChrome/lighthouse/issues/12295#issue-840261460 for more.
const estimateFromJpeg = otherFormatSizes.jpegSize * 5 / 10;
const estimateFromWebp = otherFormatSizes.webpSize * 8 / 10;
return estimateFromJpeg / 2 + estimateFromWebp / 2;
}
/**
* @param {LH.Artifacts} artifacts
* @return {import('./byte-efficiency-audit.js').ByteEfficiencyProduct}
*/
static audit_(artifacts) {
const pageURL = artifacts.URL.finalDisplayedUrl;
const images = artifacts.OptimizedImages;
const imageElements = artifacts.ImageElements;
/** @type {Map<string, LH.Artifacts.ImageElement>} */
const imageElementsByURL = new Map();
imageElements.forEach(img => imageElementsByURL.set(img.src, img));
/** @type {Array<LH.Audit.ByteEfficiencyItem>} */
const items = [];
const warnings = [];
for (const image of images) {
const imageElement = imageElementsByURL.get(image.url);
if (image.failed) {
warnings.push(`Unable to decode ${UrlUtils.getURLDisplayName(image.url)}`);
continue;
}
// Skip if the image was already using a modern format.
if (image.mimeType === 'image/webp' || image.mimeType === 'image/avif') continue;
const jpegSize = image.jpegSize;
let webpSize = image.webpSize;
let avifSize = ModernImageFormats.estimateAvifSizeFromWebPAndJpegEstimates({
jpegSize,
webpSize,
});
let fromProtocol = true;
if (typeof webpSize === 'undefined') {
if (!imageElement) {
warnings.push(`Unable to locate resource ${UrlUtils.getURLDisplayName(image.url)}`);
continue;
}
// Skip if we couldn't collect natural image size information.
if (!imageElement.naturalDimensions) continue;
const naturalHeight = imageElement.naturalDimensions.height;
const naturalWidth = imageElement.naturalDimensions.width;
// If naturalHeight or naturalWidth are falsy, information is not valid, skip.
if (!naturalWidth || !naturalHeight) continue;
webpSize = ModernImageFormats.estimateWebPSizeFromDimensions({
naturalHeight,
naturalWidth,
});
avifSize = ModernImageFormats.estimateAvifSizeFromDimensions({
naturalHeight,
naturalWidth,
});
fromProtocol = false;
}
if (webpSize === undefined || avifSize === undefined) continue;
// Visible wasted bytes uses AVIF, but we still include the WebP savings in the LHR.
const wastedWebpBytes = image.originalSize - webpSize;
const wastedBytes = image.originalSize - avifSize;
if (wastedBytes < IGNORE_THRESHOLD_IN_BYTES) continue;
const url = UrlUtils.elideDataURI(image.url);
const isCrossOrigin = !UrlUtils.originsMatch(pageURL, image.url);
items.push({
node: imageElement ? ByteEfficiencyAudit.makeNodeItem(imageElement.node) : undefined,
url,
fromProtocol,
isCrossOrigin,
totalBytes: image.originalSize,
wastedBytes,
wastedWebpBytes,
});
}
/** @type {LH.Audit.Details.Opportunity['headings']} */
const headings = [
{key: 'node', valueType: 'node', label: ''},
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
{key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
];
return {
warnings,
items,
headings,
};
}
}
export default ModernImageFormats;
export {UIStrings};