diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js index 1dac9573df02..891c590c6442 100644 --- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js @@ -33,6 +33,9 @@ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); const IGNORE_THRESHOLD_IN_BYTES = 4096; +// Ignore up to 12KB of waste if an effort was made with breakpoints. +const IGNORE_THRESHOLD_IN_BYTES_BREAKPOINTS_PRESENT = 12288; + class UsesResponsiveImages extends ByteEfficiencyAudit { /** * @return {LH.Audit.Meta} @@ -112,6 +115,18 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { }; } + + /** + * @param {LH.Artifacts.ImageElement} image + * @return {number}; + */ + static determineAllowableWaste(image) { + if (image.srcset || image.isPicture) { + return IGNORE_THRESHOLD_IN_BYTES_BREAKPOINTS_PRESENT; + } + return IGNORE_THRESHOLD_IN_BYTES; + } + /** * @param {LH.Artifacts} artifacts * @param {Array} networkRecords @@ -126,6 +141,8 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { const ViewportDimensions = artifacts.ViewportDimensions; /** @type {Map} */ const resultsMap = new Map(); + /** @type {Array} */ + const passedImageList = []; for (const image of images) { // Give SVG a free pass because creating a "responsive" SVG is of questionable value. // Ignore CSS images because it's difficult to determine what is a spritesheet, @@ -147,15 +164,23 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { ); if (!processed) continue; - // Don't warn about an image that was later used appropriately + // Verify the image wastes more than the minimum. + const exceedsAllowableWaste = processed.wastedBytes > this.determineAllowableWaste(image); + const existing = resultsMap.get(processed.url); - if (!existing || existing.wastedBytes > processed.wastedBytes) { - resultsMap.set(processed.url, processed); + // Don't warn about an image that was later used appropriately, or wastes a trivial amount of data. + if (exceedsAllowableWaste && !passedImageList.includes(processed.url)) { + if ((!existing || existing.wastedBytes > processed.wastedBytes)) { + resultsMap.set(processed.url, processed); + } + } else { + // Ensure this url passes for future tests. + resultsMap.delete(processed.url); + passedImageList.push(processed.url); } } - const items = Array.from(resultsMap.values()) - .filter(item => item.wastedBytes > IGNORE_THRESHOLD_IN_BYTES); + const items = Array.from(resultsMap.values()); /** @type {LH.Audit.Details.Opportunity['headings']} */ const headings = [ diff --git a/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js b/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js index 97057ead95fa..d765e916376e 100644 --- a/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js +++ b/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js @@ -28,8 +28,15 @@ function generateSize(width, height, prefix = 'displayed') { return size; } -function generateImage(clientSize, naturalDimensions, src = 'https://google.com/logo.png') { - return {src, ...clientSize, naturalDimensions, node: {devtoolsNodePath: '1,HTML,1,IMG'}}; +function generateImage(clientSize, naturalDimensions, src = 'https://google.com/logo.png', srcset = '', isPicture = false) { + return { + src, + srcset, + ...clientSize, + naturalDimensions, + isPicture, + node: {devtoolsNodePath: '1,HTML,1,IMG'}, + }; } describe('Page uses responsive images', () => { @@ -132,7 +139,13 @@ describe('Page uses responsive images', () => { }); it('identifies when images are not wasteful', async () => { - const networkRecords = [generateRecord(100, 300, 'https://google.com/logo.png'), generateRecord(90, 500, 'https://google.com/logo2.png'), generateRecord(20, 100, 'data:image/jpeg;base64,foobar')]; + const networkRecords = [ + generateRecord(100, 300, 'https://google.com/logo.png'), + generateRecord(90, 500, 'https://google.com/logo2.png'), + generateRecord(100, 300, 'https://google.com/logo3a.png'), + generateRecord(100, 300, 'https://google.com/logo3b.png'), + generateRecord(100, 300, 'https://google.com/logo3c.png'), + generateRecord(20, 100, 'data:image/jpeg;base64,foobar')]; const auditResult = await UsesResponsiveImagesAudit.audit_({ ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 2}, ImageElements: [ @@ -146,6 +159,26 @@ describe('Page uses responsive images', () => { {width: 210, height: 210}, 'https://google.com/logo2.png' ), + generateImage( + generateSize(100, 100), + {width: 210, height: 210}, + 'https://google.com/logo3a.png', + '', + false + ), + generateImage( + generateSize(100, 100), + {width: 210, height: 210}, + 'https://google.com/logo3b.png', + '', + true + ), + generateImage( + generateSize(100, 100), + {width: 210, height: 210}, + 'https://google.com/logo3c.png', + 'https://google.com/logo3c.png https://google.com/logo3c-2x.png 2x' + ), generateImage( generateSize(100, 100), {width: 80, height: 80}, @@ -157,7 +190,10 @@ describe('Page uses responsive images', () => { {computedCache: new Map()} ); - assert.equal(auditResult.items.length, 2); + assert.equal(auditResult.items.length, 3); + assert.equal(auditResult.items[0].url, 'https://google.com/logo.png'); + assert.equal(auditResult.items[1].url, 'https://google.com/logo3a.png'); + assert.equal(auditResult.items[2].url, 'https://google.com/logo2.png'); }); it('ignores vectors', async () => {