Skip to content

Commit

Permalink
feat(image-usage): add support for CSS images (#1868)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce authored and paulirish committed Mar 30, 2017
1 parent 6898d09 commit ef52025
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 18 deletions.
28 changes: 24 additions & 4 deletions lighthouse-cli/test/fixtures/byte-efficiency/tester.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@
setTimeout(() => {
const template = document.getElementById('lazily-loaded-image');
document.body.appendChild(template.content.cloneNode(true));
}, 3000);
}, 6000);
</script>
<style>
.onscreen {
position: absolute;
top: 0;
left: 0;
}
</style>
</head>

<body>
Expand Down Expand Up @@ -99,7 +106,20 @@ <h2>Byte efficiency tester page</h2>
<!-- PASS(optimized): image has insignificant WebP savings -->
<!-- PASS(responsive): image is used at full size -->
<!-- PASS(offscreen): image is onscreen -->
<img style="position: absolute; top: 0; left: 0;" src="lighthouse-320x212-poor.jpg?duplicate">
<img class="onscreen" src="lighthouse-320x212-poor.jpg?duplicate">

<!-- PASS(optimized): image is WebP -->
<!-- FAIL(responsive): image is used at 1/16 size -->
<!-- PASS(offscreen): image is onscreen -->
<div class="onscreen" style="width: 120px; height: 80px; background: 50% 50% url(lighthouse-480x320.webp?css);"></div>

<!-- PASSWARN(optimized): image is JPEG optimized but not WebP -->
<!-- PASS(responsive): image is used numerous times in sprite-fashion -->
<!-- PASS(offscreen): image is onscreen -->
<div class="onscreen" style="width: 30px; height: 30px; background: 0% 50% url(lighthouse-480x320.jpg?sprite);"></div>
<div class="onscreen" style="width: 30px; height: 30px; background: 25% 50% url(lighthouse-480x320.jpg?sprite);"></div>
<div class="onscreen" style="width: 30px; height: 30px; background: 50% 50% url(lighthouse-480x320.jpg?sprite);"></div>
<div class="onscreen" style="width: 30px; height: 30px; background: 75% 50% url(lighthouse-480x320.jpg?sprite);"></div>
</div>

<template id="lazily-loaded-image">
Expand All @@ -120,7 +140,7 @@ <h2>Byte efficiency tester page</h2>
generateInlineScriptWithSize(24000, '.definitely-unused { background: none; }\n');
</script>

<!-- Ensure the page takes at least 5 seconds and we don't exit before the lazily loaded image -->
<script src="delay-complete.js?delay=5000"></script>
<!-- Ensure the page takes at least 7 seconds and we don't exit before the lazily loaded image -->
<script src="delay-complete.js?delay=7000"></script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ module.exports = [
extendedInfo: {
value: {
results: {
length: 4
length: 5
}
}
}
Expand All @@ -60,7 +60,7 @@ module.exports = [
extendedInfo: {
value: {
results: {
length: 2
length: 3
}
}
}
Expand Down
68 changes: 56 additions & 12 deletions lighthouse-core/gather/gatherers/image-usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,73 @@

const Gatherer = require('./gatherer');

/* global document, Image */
/* global window, document, Image */

/* istanbul ignore next */
function collectImageElementInfo() {
return [...document.querySelectorAll('img')].map(element => {
function getClientRect(element) {
const clientRect = element.getBoundingClientRect();
return {
// manually copy the properties because ClientRect does not JSONify
top: clientRect.top,
bottom: clientRect.bottom,
left: clientRect.left,
right: clientRect.right,
};
}

const htmlImages = [...document.querySelectorAll('img')].map(element => {
return {
// currentSrc used over src to get the url as determined by the browser
// after taking into account srcset/media/sizes/etc.
src: element.currentSrc,
clientWidth: element.clientWidth,
clientHeight: element.clientHeight,
clientRect: {
// manually copy the properties because ClientRect does not JSONify
top: clientRect.top,
bottom: clientRect.bottom,
left: clientRect.left,
right: clientRect.right,
},
clientRect: getClientRect(element),
naturalWidth: element.naturalWidth,
naturalHeight: element.naturalHeight,
isCss: false,
isPicture: element.parentElement.tagName === 'PICTURE',
};
});

// 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 = [...document.querySelectorAll('html /deep/ *')].reduce((images, element) => {
const style = window.getComputedStyle(element);
if (!CSS_URL_REGEX.test(style.backgroundImage) ||
!CSS_SIZE_REGEX.test(style.backgroundSize)) {
return images;
}

const imageMatch = style.backgroundImage.match(CSS_URL_REGEX);
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,
clientWidth: element.clientWidth,
clientHeight: element.clientHeight,
clientRect: getClientRect(element),
// CSS Images do not expose natural size, we'll determine the size later
naturalWidth: Number.MAX_VALUE,
naturalHeight: Number.MAX_VALUE,
isCss: true,
isPicture: false,
});

return images;
}, []);

return htmlImages.concat(cssImages);
}

/* istanbul ignore next */
Expand Down Expand Up @@ -103,9 +146,10 @@ class ImageUsage extends Gatherer {
// link up the image with its network record
element.networkRecord = indexedNetworkRecords[element.src];

// Images within `picture` behave strangely and natural size information
// isn't accurate. Try to get the actual size if we can.
const elementPromise = element.isPicture && element.networkRecord ?
// Images within `picture` behave strangely and natural size information isn't accurate,
// CSS images have no natural size information at all.
// Try to get the actual size if we can.
const elementPromise = (element.isPicture || element.isCss) && element.networkRecord ?
this.fetchElementWithSizeInformation(element) :
Promise.resolve(element);

Expand Down

0 comments on commit ef52025

Please sign in to comment.