core(responsive-images): find offscreen images larger than viewport (#10506)
This commit is contained in:
Родитель
ec45d53f3f
Коммит
2f11010a8a
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-2048x1356.webp
поставляемый
Normal file
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-2048x1356.webp
поставляемый
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 41 KiB |
|
@ -85,15 +85,21 @@ setTimeout(() => {
|
|||
|
||||
<!-- PASS(optimized): image is JPEG optimized -->
|
||||
<!-- FAIL(webp): image is not WebP optimized -->
|
||||
<!-- FAIL(responsive): image is 25% used at DPR 2 -->
|
||||
<!-- FAIL(responsive): image is 50% used at DPR 3 -->
|
||||
<!-- PASS(offscreen): image is onscreen -->
|
||||
<img style="width: 256px; height: 170px;" src="lighthouse-1024x680.jpg">
|
||||
|
||||
<!-- PASS(optimized): image is JPEG optimized -->
|
||||
<!-- PASS(webp): image is WebP optimized -->
|
||||
<!-- FAIL(responsive): image is 25% used at DPR 3 -->
|
||||
<!-- PASS(offscreen): image is offscreen but lazily loaded -->
|
||||
<img style="width: 0px; height: 0px;" src="lighthouse-2048x1356.webp?size0" loading="lazy">
|
||||
|
||||
<!-- PASS(optimized): image is JPEG optimized -->
|
||||
<!-- FAIL(webp): image is not WebP optimized -->
|
||||
<!-- PASS(responsive): image is fully used at DPR 2 -->
|
||||
<!-- PASS(responsive): image is fully used at DPR 3 -->
|
||||
<!-- PASS(offscreen): image is onscreen -->
|
||||
<img style="width: 240px; height: 160px;" src="lighthouse-480x320.jpg">
|
||||
<img style="width: 160px; height: 110px;" src="lighthouse-480x320.jpg">
|
||||
|
||||
<!-- PASS(optimized): image is JPEG optimized -->
|
||||
<!-- PASS(webp): image has insigificant WebP savings -->
|
||||
|
@ -103,7 +109,7 @@ setTimeout(() => {
|
|||
|
||||
<!-- PASS(optimized): image is JPEG optimized -->
|
||||
<!-- PASS(webp): image is WebP optimized -->
|
||||
<!-- FAIL(responsive): image is 25% used at DPR 2 (but small savings) -->
|
||||
<!-- FAIL(responsive): image is 50% used at DPR 3 (but small savings) -->
|
||||
<!-- FAIL(offscreen): image is offscreen -->
|
||||
<img style="margin-top: 1000px; width: 120px; height: 80px;" src="lighthouse-480x320.webp">
|
||||
|
||||
|
|
|
@ -212,13 +212,13 @@ const expectations = [
|
|||
},
|
||||
},
|
||||
'uses-responsive-images': {
|
||||
displayValue: 'Potential savings of 53\xa0KB',
|
||||
details: {
|
||||
overallSavingsBytes: '>50000',
|
||||
overallSavingsBytes: '82000 +/- 5000',
|
||||
items: {
|
||||
0: {wastedPercent: '<46'},
|
||||
1: {wastedPercent: '<46'},
|
||||
length: 2,
|
||||
0: {wastedPercent: '45 +/- 5', url: /lighthouse-1024x680.jpg/},
|
||||
1: {wastedPercent: '72 +/- 5', url: /lighthouse-2048x1356.webp\?size0/},
|
||||
2: {wastedPercent: '45 +/- 5', url: /lighthouse-480x320.webp/},
|
||||
length: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -48,28 +48,42 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
|
|||
|
||||
/**
|
||||
* @param {LH.Artifacts.ImageElement} image
|
||||
* @param {number} DPR devicePixelRatio
|
||||
* @param {LH.Artifacts.ViewportDimensions} ViewportDimensions
|
||||
* @return {null|Error|LH.Audit.ByteEfficiencyItem};
|
||||
*/
|
||||
static computeWaste(image, DPR) {
|
||||
static computeWaste(image, ViewportDimensions) {
|
||||
// Nothing can be done without network info.
|
||||
if (!image.resourceSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let usedPixels = image.displayedWidth * image.displayedHeight *
|
||||
Math.pow(ViewportDimensions.devicePixelRatio, 2);
|
||||
// If the image has 0 dimensions, it's probably hidden/offscreen, so we'll be as forgiving as possible
|
||||
// and assume it's the size of two viewports. See https://github.com/GoogleChrome/lighthouse/issues/7236
|
||||
if (!usedPixels) {
|
||||
const viewportWidth = ViewportDimensions.innerWidth;
|
||||
const viewportHeight = ViewportDimensions.innerHeight * 2;
|
||||
const imageAspectRatio = image.naturalWidth / image.naturalHeight;
|
||||
const viewportAspectRatio = viewportWidth / viewportHeight;
|
||||
let usedViewportWidth = viewportWidth;
|
||||
let usedViewportHeight = viewportHeight;
|
||||
if (imageAspectRatio > viewportAspectRatio) {
|
||||
usedViewportHeight = viewportWidth / imageAspectRatio;
|
||||
} else {
|
||||
usedViewportWidth = viewportHeight * imageAspectRatio;
|
||||
}
|
||||
|
||||
usedPixels = usedViewportWidth * usedViewportHeight *
|
||||
Math.pow(ViewportDimensions.devicePixelRatio, 2);
|
||||
}
|
||||
|
||||
const url = URL.elideDataURI(image.src);
|
||||
const actualPixels = image.naturalWidth * image.naturalHeight;
|
||||
const usedPixels = image.displayedWidth * image.displayedHeight * Math.pow(DPR, 2);
|
||||
const wastedRatio = 1 - (usedPixels / actualPixels);
|
||||
const totalBytes = image.resourceSize;
|
||||
const wastedBytes = Math.round(totalBytes * wastedRatio);
|
||||
|
||||
// If the image has 0 dimensions, it's probably hidden/offscreen, so let the offscreen-images
|
||||
// audit handle it instead.
|
||||
if (!usedPixels) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(wastedRatio)) {
|
||||
return new Error(`Invalid image sizing information ${url}`);
|
||||
}
|
||||
|
@ -88,7 +102,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
|
|||
*/
|
||||
static audit_(artifacts) {
|
||||
const images = artifacts.ImageElements;
|
||||
const DPR = artifacts.ViewportDimensions.devicePixelRatio;
|
||||
const ViewportDimensions = artifacts.ViewportDimensions;
|
||||
|
||||
/** @type {string[]} */
|
||||
const warnings = [];
|
||||
|
@ -103,7 +117,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
|
|||
continue;
|
||||
}
|
||||
|
||||
const processed = UsesResponsiveImages.computeWaste(image, DPR);
|
||||
const processed = UsesResponsiveImages.computeWaste(image, ViewportDimensions);
|
||||
if (!processed) continue;
|
||||
|
||||
if (processed instanceof Error) {
|
||||
|
|
|
@ -38,7 +38,11 @@ describe('Page uses responsive images', () => {
|
|||
const description = `identifies when an image is ${condition}`;
|
||||
it(description, () => {
|
||||
const result = UsesResponsiveImagesAudit.audit_({
|
||||
ViewportDimensions: {devicePixelRatio: data.devicePixelRatio || 1},
|
||||
ViewportDimensions: {
|
||||
innerWidth: 1000,
|
||||
innerHeight: 1000,
|
||||
devicePixelRatio: data.devicePixelRatio || 1,
|
||||
},
|
||||
ImageElements: [
|
||||
generateImage(
|
||||
generateSize(...data.clientSize),
|
||||
|
@ -89,9 +93,26 @@ describe('Page uses responsive images', () => {
|
|||
sizeInKb: 1,
|
||||
});
|
||||
|
||||
testImage('offscreen and within viewport size', {
|
||||
listed: false,
|
||||
devicePixelRatio: 2,
|
||||
clientSize: [0, 0], // 0 dimensions will be treated as 2 viewport sized
|
||||
naturalSize: [2000, 3000],
|
||||
sizeInKb: 1000,
|
||||
});
|
||||
|
||||
testImage('offscreen and larger than viewport size', {
|
||||
listed: true,
|
||||
devicePixelRatio: 2,
|
||||
clientSize: [0, 0], // 0 dimensions will be treated as 2 viewport sized
|
||||
naturalSize: [5000, 5000],
|
||||
sizeInKb: 1000,
|
||||
expectedWaste: 840, // 1000 * 21/25
|
||||
});
|
||||
|
||||
it('handles images without network record', () => {
|
||||
const auditResult = UsesResponsiveImagesAudit.audit_({
|
||||
ViewportDimensions: {devicePixelRatio: 2},
|
||||
ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 2},
|
||||
ImageElements: [
|
||||
generateImage(
|
||||
generateSize(100, 100),
|
||||
|
@ -106,7 +127,7 @@ describe('Page uses responsive images', () => {
|
|||
|
||||
it('identifies when images are not wasteful', () => {
|
||||
const auditResult = UsesResponsiveImagesAudit.audit_({
|
||||
ViewportDimensions: {devicePixelRatio: 2},
|
||||
ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 2},
|
||||
ImageElements: [
|
||||
generateImage(
|
||||
generateSize(200, 200),
|
||||
|
@ -138,7 +159,7 @@ describe('Page uses responsive images', () => {
|
|||
const recordA = generateRecord(100, 300, 'image/svg+xml');
|
||||
|
||||
const auditResult = UsesResponsiveImagesAudit.audit_({
|
||||
ViewportDimensions: {devicePixelRatio: 1},
|
||||
ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 1},
|
||||
ImageElements: [
|
||||
generateImage(generateSize(10, 10), naturalSizeA, recordA, urlA),
|
||||
],
|
||||
|
@ -153,7 +174,7 @@ describe('Page uses responsive images', () => {
|
|||
const recordA = generateRecord(100, 300);
|
||||
|
||||
const auditResult = UsesResponsiveImagesAudit.audit_({
|
||||
ViewportDimensions: {devicePixelRatio: 1},
|
||||
ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 1},
|
||||
ImageElements: [
|
||||
{...generateImage(generateSize(10, 10), naturalSizeA, recordA, urlA), isCss: true},
|
||||
],
|
||||
|
@ -168,7 +189,7 @@ describe('Page uses responsive images', () => {
|
|||
const recordA = generateRecord(100, 300);
|
||||
|
||||
const auditResult = UsesResponsiveImagesAudit.audit_({
|
||||
ViewportDimensions: {devicePixelRatio: 1},
|
||||
ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 1},
|
||||
ImageElements: [
|
||||
generateImage(generateSize(10, 10), naturalSizeA, recordA, urlA),
|
||||
],
|
||||
|
@ -187,7 +208,7 @@ describe('Page uses responsive images', () => {
|
|||
const recordB = generateRecord(10, 20); // make it small to still test passing
|
||||
|
||||
const auditResult = UsesResponsiveImagesAudit.audit_({
|
||||
ViewportDimensions: {devicePixelRatio: 1},
|
||||
ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 1},
|
||||
ImageElements: [
|
||||
generateImage(generateSize(10, 10), naturalSizeA, recordA, urlA),
|
||||
generateImage(generateSize(450, 450), naturalSizeA, recordA, urlA),
|
||||
|
|
Загрузка…
Ссылка в новой задаче