From a014e5a50a531461976d841b7931e98077f812c2 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Mon, 13 May 2019 10:34:03 -0500 Subject: [PATCH 1/4] core(image-elements): drop spritesheet logic --- .../test/fixtures/byte-efficiency/tester.html | 12 +-- .../byte-efficiency/expectations.js | 14 +-- .../byte-efficiency/uses-responsive-images.js | 17 ++-- .../gather/gatherers/image-elements.js | 85 +++++++++++-------- 4 files changed, 73 insertions(+), 55 deletions(-) diff --git a/lighthouse-cli/test/fixtures/byte-efficiency/tester.html b/lighthouse-cli/test/fixtures/byte-efficiency/tester.html index 17e1372dcde4..15df1431bd00 100644 --- a/lighthouse-cli/test/fixtures/byte-efficiency/tester.html +++ b/lighthouse-cli/test/fixtures/byte-efficiency/tester.html @@ -102,7 +102,7 @@

Byte efficiency tester page

- + @@ -130,15 +130,15 @@

Byte efficiency tester page

- - - + + + -
+
- +
diff --git a/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js b/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js index 3da2d9062e2e..fdeebcae9fad 100644 --- a/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js +++ b/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js @@ -83,7 +83,7 @@ module.exports = [ details: { overallSavingsBytes: '>60000', items: { - length: 4, + length: 5, }, }, }, @@ -106,14 +106,14 @@ module.exports = [ }, }, 'uses-responsive-images': { - displayValue: 'Potential savings of 75\xa0KB', + displayValue: 'Potential savings of 69\xa0KB', details: { overallSavingsBytes: '>75000', - items: [ - {wastedPercent: '<60'}, - {wastedPercent: '<60'}, - {wastedPercent: '<60'}, - ], + items: { + 0: {wastedPercent: '<60'}, + 1: {wastedPercent: '<60'}, + length: 2, + }, }, }, }, diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js index 139f26c733a2..8609b73c1286 100644 --- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js @@ -94,19 +94,22 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { const warnings = []; /** @type {Map} */ const resultsMap = new Map(); - images.forEach(image => { - // TODO: give SVG a free pass until a detail per pixel metric is available - if (!image.resourceSize || image.mimeType === 'image/svg+xml') { - return; + for (const image of images) { + // Ignore images without resource size information. + // 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, + // and I mean just c'mon https://css-tricks.com/responsive-images-css/. + if (!image.resourceSize || image.mimeType === 'image/svg+xml' || image.isCss) { + continue; } const processed = UsesResponsiveImages.computeWaste(image, DPR); - if (!processed) return; + if (!processed) continue; if (processed instanceof Error) { warnings.push(processed.message); Sentry.captureException(processed, {tags: {audit: this.meta.id}, level: 'warning'}); - return; + continue; } // Don't warn about an image that was later used appropriately @@ -114,7 +117,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { if (!existing || existing.wastedBytes > processed.wastedBytes) { resultsMap.set(processed.url, processed); } - }); + } const items = Array.from(resultsMap.values()) .filter(item => item.wastedBytes > IGNORE_THRESHOLD_IN_BYTES); diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js index 3a49b0f241e2..3c321a2b3c90 100644 --- a/lighthouse-core/gather/gatherers/image-elements.js +++ b/lighthouse-core/gather/gatherers/image-elements.js @@ -15,30 +15,31 @@ const Driver = require('../driver.js'); // eslint-disable-line no-unused-vars /* global window, getElementsInDocument, Image */ -/** @return {Array} */ + +/** @param {Element} element */ /* istanbul ignore next */ -function collectImageElementInfo() { - /** @param {Element} element */ - function getClientRect(element) { - const clientRect = element.getBoundingClientRect(); - return { - // Just grab the DOMRect properties we want, excluding x/y/width/height - top: clientRect.top, - bottom: clientRect.bottom, - left: clientRect.left, - right: clientRect.right, - }; - } +function getClientRect(element) { + const clientRect = element.getBoundingClientRect(); + return { + // Just grab the DOMRect properties we want, excluding x/y/width/height + top: clientRect.top, + bottom: clientRect.bottom, + left: clientRect.left, + right: clientRect.right, + }; +} - /** @type {Array} */ - // @ts-ignore - added by getElementsInDocumentFnString - const allElements = getElementsInDocument(); +/** + * @param {Array} allElements + * @return {Array} + */ +/* istanbul ignore next */ +function getHTMLImages(allElements) { const allImageElements = /** @type {Array} */ (allElements.filter(element => { return element.localName === 'img'; })); - /** @type {Array} */ - const htmlImages = allImageElements.map(element => { + return allImageElements.map(element => { const computedStyle = window.getComputedStyle(element); return { // currentSrc used over src to get the url as determined by the browser @@ -57,30 +58,30 @@ function collectImageElementInfo() { ), }; }); +} +/** + * @param {Array} allElements + * @return {Array} + */ +/* istanbul ignore next */ +function getCSSImages(allElements) { // Chrome normalizes background image style from getComputedStyle to be an absolute URL in quotes. // Only match basic background-image: url("http://host/image.jpeg") declarations const CSS_URL_REGEX = /^url\("([^"]+)"\)$/; - // Only find images that aren't specifically scaled - const CSS_SIZE_REGEX = /(auto|contain|cover)/; - const cssImages = allElements.reduce((images, element) => { + /** @type {Array} */ + const images = []; + + for (const element of allElements) { const style = window.getComputedStyle(element); - if (!style.backgroundImage || !CSS_URL_REGEX.test(style.backgroundImage) || - !style.backgroundSize || !CSS_SIZE_REGEX.test(style.backgroundSize)) { - return images; - } + // If the element didn't have a CSS background image, we're not interested. + if (!style.backgroundImage || !CSS_URL_REGEX.test(style.backgroundImage)) continue; const imageMatch = style.backgroundImage.match(CSS_URL_REGEX); // @ts-ignore test() above ensures that there is a match. const url = imageMatch[1]; - // Heuristic to filter out sprite sheets - const differentImages = images.filter(image => image.src !== url); - if (images.length - differentImages.length > 2) { - return differentImages; - } - images.push({ src: url, displayedWidth: element.clientWidth, @@ -94,11 +95,18 @@ function collectImageElementInfo() { usesObjectFit: false, resourceSize: 0, // this will get overwritten below }); + } - return images; - }, /** @type {Array} */ ([])); + return images; +} - return htmlImages.concat(cssImages); +/** @return {Array} */ +/* istanbul ignore next */ +function collectImageElementInfo() { + /** @type {Array} */ + // @ts-ignore - added by getElementsInDocumentFnString + const allElements = getElementsInDocument(); + return getHTMLImages(allElements).concat(getCSSImages(allElements)); } /** @@ -160,16 +168,23 @@ class ImageElements extends Gatherer { const expression = `(function() { ${pageFunctions.getElementsInDocumentString}; // define function on page - return (${collectImageElementInfo.toString()})(); + ${getClientRect.toString()}; + ${getHTMLImages.toString()}; + ${getCSSImages.toString()}; + ${collectImageElementInfo.toString()}; + + return collectImageElementInfo(); })()`; /** @type {Array} */ const elements = await driver.evaluateAsync(expression); + /** @type {Array} */ const imageUsage = []; const top50Images = Object.values(indexedNetworkRecords) .sort((a, b) => b.resourceSize - a.resourceSize) .slice(0, 50); + for (let element of elements) { // Pull some of our information directly off the network record. const networkRecord = indexedNetworkRecords[element.src] || {}; From eb7e91600330702808c8b6b9021e86e2377d8fd7 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Thu, 16 May 2019 09:36:56 -0500 Subject: [PATCH 2/4] update smoke expectations --- lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js b/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js index fdeebcae9fad..649b674e44ae 100644 --- a/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js +++ b/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js @@ -108,7 +108,7 @@ module.exports = [ 'uses-responsive-images': { displayValue: 'Potential savings of 69\xa0KB', details: { - overallSavingsBytes: '>75000', + overallSavingsBytes: '>65000', items: { 0: {wastedPercent: '<60'}, 1: {wastedPercent: '<60'}, From 5ed13c49703d869622da2ea8878e29c85dab4d96 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Thu, 16 May 2019 10:23:10 -0500 Subject: [PATCH 3/4] more tests --- .../uses-responsive-images-test.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 68aec09f8811..a10e7b69717d 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 @@ -147,6 +147,37 @@ describe('Page uses responsive images', () => { assert.equal(auditResult.items.length, 0); }); + it('ignores CSS', () => { + const urlA = 'https://google.com/logo.png'; + const naturalSizeA = generateSize(450, 450, 'natural'); + const recordA = generateRecord(100, 300); + + const auditResult = UsesResponsiveImagesAudit.audit_({ + ViewportDimensions: {devicePixelRatio: 1}, + ImageElements: [ + {...generateImage(generateSize(10, 10), naturalSizeA, recordA, urlA), isCss: true}, + ], + }); + + assert.equal(auditResult.items.length, 0); + }); + + it('handles failure', () => { + const urlA = 'https://google.com/logo.png'; + const naturalSizeA = generateSize(NaN, 450, 'natural'); + const recordA = generateRecord(100, 300); + + const auditResult = UsesResponsiveImagesAudit.audit_({ + ViewportDimensions: {devicePixelRatio: 1}, + ImageElements: [ + generateImage(generateSize(10, 10), naturalSizeA, recordA, urlA), + ], + }); + + assert.equal(auditResult.items.length, 0); + assert.equal(auditResult.warnings.length, 1); + }); + it('de-dupes images', () => { const urlA = 'https://google.com/logo.png'; const naturalSizeA = generateSize(450, 450, 'natural'); From 0dab0e5a9eff5b3c8d743ddee7d53bb43c458494 Mon Sep 17 00:00:00 2001 From: Patrick Hulce Date: Thu, 23 May 2019 09:42:46 -0500 Subject: [PATCH 4/4] Update lighthouse-core/audits/byte-efficiency/uses-responsive-images.js --- .../audits/byte-efficiency/uses-responsive-images.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js index 8609b73c1286..6bf2bb16a448 100644 --- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js @@ -98,7 +98,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { // Ignore images without resource size information. // 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, - // and I mean just c'mon https://css-tricks.com/responsive-images-css/. + // and the reward-to-effort ratio for responsive CSS images is quite low https://css-tricks.com/responsive-images-css/. if (!image.resourceSize || image.mimeType === 'image/svg+xml' || image.isCss) { continue; }