refactor: DRY byte efficiency audits (#1635)
* refactor: DRY byte efficiency tests * loosen responsive images conditions * reduce optimized images aggressiveness * consistency with optimized images threshold * more updates * add class overview * update unused css description * more feedback * added smoke tests * adjust perf.json * pauls feedback
This commit is contained in:
Родитель
e72aadccf5
Коммит
d1b91101a8
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-1024x680.jpg
поставляемый
Normal file
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-1024x680.jpg
поставляемый
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 81 KiB |
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-320x212-poor.jpg
поставляемый
Normal file
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-320x212-poor.jpg
поставляемый
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 6.0 KiB |
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-480x320.jpg
поставляемый
Normal file
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-480x320.jpg
поставляемый
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 18 KiB |
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-480x320.webp
поставляемый
Normal file
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-480x320.webp
поставляемый
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 11 KiB |
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-unoptimized.jpg
поставляемый
Normal file
Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-unoptimized.jpg
поставляемый
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 51 KiB |
|
@ -0,0 +1,87 @@
|
|||
<!doctype html>
|
||||
<!--
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Byte Efficency tester</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
<script>
|
||||
function generateInlineScriptWithSize(sizeInBytes, firstContent = '', used = false) {
|
||||
const data = [];
|
||||
while (sizeInBytes > 0) {
|
||||
const index = Math.round(100000 * Math.random());
|
||||
const className = `${used ? '' : 'un'}used-selector${index}`;
|
||||
const rule = `.${className} { background: white; }`;
|
||||
data.push(rule);
|
||||
if (used) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add(className);
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
sizeInBytes -= rule.length / 3; // 1 byte per character, GZip estimate is 3x
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
const textContent = firstContent + data.join('\n');
|
||||
style.appendChild(document.createTextNode(textContent));
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<h2>Byte efficiency tester page</h2>
|
||||
<span>Hi there!</span>
|
||||
</div>
|
||||
|
||||
<div class="images">
|
||||
<!-- FAIL(optimized): image is not JPEG optimized -->
|
||||
<!-- PASS(responsive): image is used at full size -->
|
||||
<img src="lighthouse-unoptimized.jpg">
|
||||
|
||||
<!-- PASSWARN(optimized): image is JPEG optimized but not WebP -->
|
||||
<!-- FAIL(responsive): image is 25% used at DPR 2 -->
|
||||
<img style="width: 256px; height: 170px;" src="lighthouse-1024x680.jpg">
|
||||
|
||||
<!-- PASSWARN(optimized): image is JPEG optimized but not WebP -->
|
||||
<!-- PASS(responsive): image is fully used at DPR 2 -->
|
||||
<img style="width: 240px; height: 160px;" src="lighthouse-480x320.jpg">
|
||||
|
||||
<!-- PASS(optimized): image has insignificant WebP savings -->
|
||||
<!-- PASS(responsive): image is used at full size -->
|
||||
<img src="lighthouse-320x212-poor.jpg">
|
||||
|
||||
<!-- PASS(optimized): image is fully optimized -->
|
||||
<!-- PASSWARN(responsive): image is 25% used at DPR 2 (but small savings) -->
|
||||
<img style="width: 120px; height: 80px;" src="lighthouse-480x320.webp">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// PASS: unused but too small savings
|
||||
generateInlineScriptWithSize(512, '.too-small { background: none; }\n');
|
||||
// PASS: used
|
||||
generateInlineScriptWithSize(4000, '.mostly-used { background: none; }\n', true);
|
||||
// PASSWARN: unused and a bit of savings
|
||||
generateInlineScriptWithSize(2000, '.kinda-unused { background: none; }\n');
|
||||
// FAIL: unused and lots of savings
|
||||
generateInlineScriptWithSize(24000, '.definitely-unused { background: none; }\n');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Expected Lighthouse audit values for byte efficiency tests
|
||||
*/
|
||||
module.exports = [
|
||||
{
|
||||
initialUrl: 'http://localhost:10200/byte-efficiency/tester.html',
|
||||
url: 'http://localhost:10200/byte-efficiency/tester.html',
|
||||
audits: {
|
||||
'unused-css-rules': {
|
||||
score: false,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
results: {
|
||||
length: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'uses-optimized-images': {
|
||||
score: false,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
results: {
|
||||
length: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'uses-responsive-images': {
|
||||
score: false,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
results: {
|
||||
length: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
node lighthouse-cli/test/fixtures/static-server.js &
|
||||
|
||||
sleep 0.5s
|
||||
|
||||
config="lighthouse-core/config/default.json"
|
||||
expectations="lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js"
|
||||
|
||||
npm run -s smokehouse -- --config-path=$config --expectations-path=$expectations
|
||||
exit_code=$?
|
||||
|
||||
# kill test servers
|
||||
kill $(jobs -p)
|
||||
|
||||
exit "$exit_code"
|
|
@ -127,16 +127,6 @@ module.exports = [
|
|||
}
|
||||
}
|
||||
},
|
||||
'unused-css-rules': {
|
||||
score: false,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
results: {
|
||||
length: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'uses-passive-event-listeners': {
|
||||
score: false,
|
||||
extendedInfo: {
|
||||
|
@ -149,26 +139,6 @@ module.exports = [
|
|||
}
|
||||
}
|
||||
},
|
||||
'uses-optimized-images': {
|
||||
score: false,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
results: {
|
||||
length: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'uses-responsive-images': {
|
||||
score: false,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
results: {
|
||||
length: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'deprecations': {
|
||||
score: false,
|
||||
extendedInfo: {
|
||||
|
@ -221,9 +191,6 @@ module.exports = [
|
|||
'script-blocking-first-paint': {
|
||||
score: true
|
||||
},
|
||||
'unused-css-rules': {
|
||||
score: true
|
||||
},
|
||||
'uses-passive-event-listeners': {
|
||||
score: true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const Audit = require('../audit');
|
||||
const Formatter = require('../../formatters/formatter');
|
||||
|
||||
const KB_IN_BYTES = 1024;
|
||||
const WASTEFUL_THRESHOLD_IN_BYTES = 20 * KB_IN_BYTES;
|
||||
|
||||
/**
|
||||
* @overview Used as the base for all byte efficiency audits. Computes total bytes
|
||||
* and estimated time saved. Subclass and override `audit_` to return results.
|
||||
*/
|
||||
class UnusedBytes extends Audit {
|
||||
/**
|
||||
* @param {number} bytes
|
||||
* @return {number}
|
||||
*/
|
||||
static bytesToKbString(bytes) {
|
||||
return Math.round(bytes / KB_IN_BYTES).toLocaleString() + ' KB';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} bytes
|
||||
* @param {percent} percent
|
||||
*/
|
||||
static toSavingsString(bytes = 0, percent = 0) {
|
||||
const kbDisplay = this.bytesToKbString(bytes);
|
||||
const percentDisplay = Math.round(percent).toLocaleString() + '%';
|
||||
return `${kbDisplay} _${percentDisplay}_`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @return {!AuditResult}
|
||||
*/
|
||||
static audit(artifacts) {
|
||||
const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS];
|
||||
return artifacts.requestNetworkThroughput(networkRecords).then(networkThroughput => {
|
||||
const result = this.audit_(artifacts);
|
||||
const debugString = result.debugString;
|
||||
const results = result.results
|
||||
.map(item => {
|
||||
item.wastedKb = this.bytesToKbString(item.wastedBytes);
|
||||
item.totalKb = this.bytesToKbString(item.totalBytes);
|
||||
item.potentialSavings = this.toSavingsString(item.wastedBytes, item.wastedPercent);
|
||||
return item;
|
||||
})
|
||||
.sort((itemA, itemB) => itemB.wastedBytes - itemA.wastedBytes);
|
||||
|
||||
const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
|
||||
// Only round to nearest 10ms since we're relatively hand-wavy
|
||||
const wastedMs = Math.round(wastedBytes / networkThroughput * 100) * 10;
|
||||
|
||||
let displayValue = '';
|
||||
if (wastedBytes) {
|
||||
const wastedKbDisplay = this.bytesToKbString(wastedBytes);
|
||||
const wastedMsDisplay = wastedMs.toLocaleString() + 'ms';
|
||||
displayValue = `Potential savings of ${wastedKbDisplay} (~${wastedMsDisplay})`;
|
||||
}
|
||||
|
||||
return this.generateAuditResult({
|
||||
debugString,
|
||||
displayValue,
|
||||
rawValue: typeof result.passes === 'undefined' ?
|
||||
wastedBytes < WASTEFUL_THRESHOLD_IN_BYTES :
|
||||
!!result.passes,
|
||||
extendedInfo: {
|
||||
formatter: Formatter.SUPPORTED_FORMATS.TABLE,
|
||||
value: {results, tableHeadings: result.tableHeadings}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @return {{results: !Array<Object>, tableHeadings: Object,
|
||||
* passes: boolean=, debugString: string=}}
|
||||
*/
|
||||
static audit_() {
|
||||
throw new Error('audit_ unimplemented');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UnusedBytes;
|
|
@ -16,13 +16,10 @@
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
const Audit = require('./audit');
|
||||
const Formatter = require('../formatters/formatter');
|
||||
const URL = require('../lib/url-shim');
|
||||
const Audit = require('./byte-efficiency-audit');
|
||||
const URL = require('../../lib/url-shim');
|
||||
|
||||
const KB_IN_BYTES = 1024;
|
||||
const PREVIEW_LENGTH = 100;
|
||||
const ALLOWABLE_UNUSED_RULES_RATIO = 0.10;
|
||||
|
||||
class UnusedCSSRules extends Audit {
|
||||
/**
|
||||
|
@ -32,7 +29,7 @@ class UnusedCSSRules extends Audit {
|
|||
return {
|
||||
category: 'CSS',
|
||||
name: 'unused-css-rules',
|
||||
description: 'Uses 90% of its CSS rules',
|
||||
description: 'Avoids loading unnecessary CSS',
|
||||
helpText: 'Remove unused rules from stylesheets to reduce unnecessary ' +
|
||||
'bytes consumed by network activity. ' +
|
||||
'[Learn more](https://developers.google.com/speed/docs/insights/OptimizeCSSDelivery)',
|
||||
|
@ -159,65 +156,37 @@ class UnusedCSSRules extends Audit {
|
|||
url,
|
||||
numUnused,
|
||||
wastedBytes,
|
||||
totalKb: Math.round(totalBytes / KB_IN_BYTES) + ' KB',
|
||||
potentialSavings: `${Math.round(percentUnused * 100)}%`,
|
||||
wastedPercent: percentUnused * 100,
|
||||
totalBytes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @return {!AuditResult}
|
||||
* @return {{results: !Array<Object>, tableHeadings: Object,
|
||||
* passes: boolean=, debugString: string=}}
|
||||
*/
|
||||
static audit(artifacts) {
|
||||
const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS];
|
||||
return artifacts.requestNetworkThroughput(networkRecords).then(networkThroughput => {
|
||||
return UnusedCSSRules.audit_(artifacts, networkThroughput);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @param {number} networkThroughput
|
||||
* @return {!AuditResult}
|
||||
*/
|
||||
static audit_(artifacts, networkThroughput) {
|
||||
static audit_(artifacts) {
|
||||
const styles = artifacts.Styles;
|
||||
const usage = artifacts.CSSUsage;
|
||||
const pageUrl = artifacts.URL.finalUrl;
|
||||
const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS];
|
||||
|
||||
const indexedSheets = UnusedCSSRules.indexStylesheetsById(styles, networkRecords);
|
||||
const unused = UnusedCSSRules.countUnusedRules(usage, indexedSheets);
|
||||
const unusedRatio = (unused / usage.length) || 0;
|
||||
UnusedCSSRules.countUnusedRules(usage, indexedSheets);
|
||||
const results = Object.keys(indexedSheets).map(sheetId => {
|
||||
return UnusedCSSRules.mapSheetToResult(indexedSheets[sheetId], pageUrl);
|
||||
}).filter(Boolean);
|
||||
}).filter(sheet => sheet && sheet.wastedBytes > 1024);
|
||||
|
||||
const wastedBytes = results.reduce((waste, result) => waste + result.wastedBytes, 0);
|
||||
let displayValue = '';
|
||||
if (unused > 0) {
|
||||
const wastedKb = Math.round(wastedBytes / KB_IN_BYTES);
|
||||
// Only round to nearest 10ms since we're relatively hand-wavy
|
||||
const wastedMs = Math.round(wastedBytes / networkThroughput * 100) * 10;
|
||||
displayValue = `${wastedKb}KB (~${wastedMs}ms) potential savings`;
|
||||
}
|
||||
|
||||
return UnusedCSSRules.generateAuditResult({
|
||||
displayValue,
|
||||
rawValue: unusedRatio < ALLOWABLE_UNUSED_RULES_RATIO,
|
||||
extendedInfo: {
|
||||
formatter: Formatter.SUPPORTED_FORMATS.TABLE,
|
||||
value: {
|
||||
results,
|
||||
tableHeadings: {
|
||||
url: 'URL',
|
||||
numUnused: 'Unused Rules',
|
||||
totalKb: 'Original (KB)',
|
||||
potentialSavings: 'Potential Savings (%)',
|
||||
}
|
||||
}
|
||||
return {
|
||||
results,
|
||||
tableHeadings: {
|
||||
url: 'URL',
|
||||
numUnused: 'Unused Rules',
|
||||
totalKb: 'Original',
|
||||
potentialSavings: 'Potential Savings',
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -18,20 +18,19 @@
|
|||
* @fileoverview This audit determines if the images used are sufficiently larger
|
||||
* than Lighthouse optimized versions of the images (as determined by the gatherer).
|
||||
* Audit will fail if one of the conditions are met:
|
||||
* * There is at least one JPEG or bitmap image that was larger than canvas encoded JPEG.
|
||||
* * There is at least one image that would have saved more than 50KB by using WebP.
|
||||
* * The savings of moving all images to WebP is greater than 100KB.
|
||||
* * There is at least one JPEG or bitmap image that was >10KB larger than canvas encoded JPEG.
|
||||
* * There is at least one image that would have saved more than 100KB by using WebP.
|
||||
* * The savings of moving all images to WebP is greater than 1MB.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const Audit = require('../audit');
|
||||
const Audit = require('./byte-efficiency-audit');
|
||||
const URL = require('../../lib/url-shim');
|
||||
const Formatter = require('../../formatters/formatter');
|
||||
|
||||
const KB_IN_BYTES = 1024;
|
||||
const IGNORE_THRESHOLD_IN_BYTES = 2 * KB_IN_BYTES;
|
||||
const TOTAL_WASTED_BYTES_THRESHOLD = 100 * KB_IN_BYTES;
|
||||
const WEBP_ALREADY_OPTIMIZED_THRESHOLD_IN_BYTES = 50 * KB_IN_BYTES;
|
||||
const IGNORE_THRESHOLD_IN_BYTES = 2048;
|
||||
const TOTAL_WASTED_BYTES_THRESHOLD = 1000 * 1024;
|
||||
const JPEG_ALREADY_OPTIMIZED_THRESHOLD_IN_BYTES = 25 * 1024;
|
||||
const WEBP_ALREADY_OPTIMIZED_THRESHOLD_IN_BYTES = 100 * 1024;
|
||||
|
||||
class UsesOptimizedImages extends Audit {
|
||||
/**
|
||||
|
@ -41,7 +40,7 @@ class UsesOptimizedImages extends Audit {
|
|||
return {
|
||||
category: 'Images',
|
||||
name: 'uses-optimized-images',
|
||||
description: 'Has optimized images',
|
||||
description: 'Avoids unoptimized images',
|
||||
helpText: 'Images should be optimized to save network bytes. ' +
|
||||
'The following images could have smaller file sizes when compressed with ' +
|
||||
'[WebP](https://developers.google.com/speed/webp/) or JPEG at 80 quality. ' +
|
||||
|
@ -53,32 +52,20 @@ class UsesOptimizedImages extends Audit {
|
|||
/**
|
||||
* @param {{originalSize: number, webpSize: number, jpegSize: number}} image
|
||||
* @param {string} type
|
||||
* @return {{bytes: number, kb: number, percent: number}}
|
||||
* @return {{bytes: number, percent: number}}
|
||||
*/
|
||||
static computeSavings(image, type) {
|
||||
const bytes = image.originalSize - image[type + 'Size'];
|
||||
const kb = Math.round(bytes / KB_IN_BYTES);
|
||||
const percent = Math.round(100 * bytes / image.originalSize);
|
||||
return {bytes, kb, percent};
|
||||
const percent = 100 * bytes / image.originalSize;
|
||||
return {bytes, percent};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @return {!AuditResult}
|
||||
* @return {{results: !Array<Object>, tableHeadings: Object,
|
||||
* passes: boolean=, debugString: string=}}
|
||||
*/
|
||||
static audit(artifacts) {
|
||||
const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS];
|
||||
return artifacts.requestNetworkThroughput(networkRecords).then(networkThroughput => {
|
||||
return UsesOptimizedImages.audit_(artifacts, networkThroughput);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @param {number} networkThroughput
|
||||
* @return {!AuditResult}
|
||||
*/
|
||||
static audit_(artifacts, networkThroughput) {
|
||||
static audit_(artifacts) {
|
||||
const images = artifacts.OptimizedImages;
|
||||
|
||||
const failedImages = [];
|
||||
|
@ -93,65 +80,57 @@ class UsesOptimizedImages extends Audit {
|
|||
return results;
|
||||
}
|
||||
|
||||
const originalKb = Math.round(image.originalSize / KB_IN_BYTES);
|
||||
const url = URL.getDisplayName(image.url);
|
||||
const webpSavings = UsesOptimizedImages.computeSavings(image, 'webp');
|
||||
|
||||
if (webpSavings.bytes > WEBP_ALREADY_OPTIMIZED_THRESHOLD_IN_BYTES) {
|
||||
hasAllEfficientImages = false;
|
||||
} else if (webpSavings.bytes < IGNORE_THRESHOLD_IN_BYTES) {
|
||||
return results;
|
||||
}
|
||||
|
||||
let jpegSavingsLabel;
|
||||
if (/(jpeg|bmp)/.test(image.mimeType)) {
|
||||
const jpegSavings = UsesOptimizedImages.computeSavings(image, 'jpeg');
|
||||
if (jpegSavings.bytes > 0) {
|
||||
if (jpegSavings.bytes > JPEG_ALREADY_OPTIMIZED_THRESHOLD_IN_BYTES) {
|
||||
hasAllEfficientImages = false;
|
||||
jpegSavingsLabel = `${jpegSavings.percent}%`;
|
||||
}
|
||||
if (jpegSavings.bytes > IGNORE_THRESHOLD_IN_BYTES) {
|
||||
jpegSavingsLabel = this.toSavingsString(jpegSavings.bytes, jpegSavings.percent);
|
||||
}
|
||||
}
|
||||
|
||||
totalWastedBytes += webpSavings.bytes;
|
||||
|
||||
results.push({
|
||||
url: URL.getDisplayName(image.url),
|
||||
url,
|
||||
preview: {url: image.url, mimeType: image.mimeType},
|
||||
total: `${originalKb} KB`,
|
||||
webpSavings: `${webpSavings.percent}%`,
|
||||
totalBytes: image.originalSize,
|
||||
wastedBytes: webpSavings.bytes,
|
||||
webpSavings: this.toSavingsString(webpSavings.bytes, webpSavings.percent),
|
||||
jpegSavings: jpegSavingsLabel
|
||||
});
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
let displayValue = '';
|
||||
if (totalWastedBytes > 1000) {
|
||||
const totalWastedKb = Math.round(totalWastedBytes / KB_IN_BYTES);
|
||||
// Only round to nearest 10ms since we're relatively hand-wavy
|
||||
const totalWastedMs = Math.round(totalWastedBytes / networkThroughput * 100) * 10;
|
||||
displayValue = `${totalWastedKb}KB (~${totalWastedMs}ms) potential savings`;
|
||||
}
|
||||
|
||||
let debugString;
|
||||
if (failedImages.length) {
|
||||
const urls = failedImages.map(image => URL.getDisplayName(image.url));
|
||||
debugString = `Lighthouse was unable to decode some of your images: ${urls.join(', ')}`;
|
||||
}
|
||||
|
||||
return UsesOptimizedImages.generateAuditResult({
|
||||
displayValue,
|
||||
return {
|
||||
passes: hasAllEfficientImages && totalWastedBytes < TOTAL_WASTED_BYTES_THRESHOLD,
|
||||
debugString,
|
||||
rawValue: hasAllEfficientImages && totalWastedBytes < TOTAL_WASTED_BYTES_THRESHOLD,
|
||||
extendedInfo: {
|
||||
formatter: Formatter.SUPPORTED_FORMATS.TABLE,
|
||||
value: {
|
||||
results,
|
||||
tableHeadings: {
|
||||
preview: '',
|
||||
url: 'URL',
|
||||
total: 'Original (KB)',
|
||||
webpSavings: 'WebP Savings (%)',
|
||||
jpegSavings: 'JPEG Savings (%)',
|
||||
}
|
||||
}
|
||||
results,
|
||||
tableHeadings: {
|
||||
preview: '',
|
||||
url: 'URL',
|
||||
totalKb: 'Original',
|
||||
webpSavings: 'WebP Savings',
|
||||
jpegSavings: 'JPEG Savings',
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -17,19 +17,18 @@
|
|||
/**
|
||||
* @fileoverview Checks to see if the images used on the page are larger than
|
||||
* their display sizes. The audit will list all images that are larger than
|
||||
* their display size regardless of DPR (a 1000px wide image displayed as a
|
||||
* 500px high-res image on a Retina display will show up as 75% unused);
|
||||
* however, the audit will only fail pages that use images that have waste
|
||||
* when computed with DPR taken into account.
|
||||
* their display size with DPR (a 1000px wide image displayed as a
|
||||
* 500px high-res image on a Retina display is 100% used);
|
||||
* However, the audit will only fail pages that use images that have waste
|
||||
* beyond a particular byte threshold.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const Audit = require('../audit');
|
||||
const Audit = require('./byte-efficiency-audit');
|
||||
const URL = require('../../lib/url-shim');
|
||||
const Formatter = require('../../formatters/formatter');
|
||||
|
||||
const KB_IN_BYTES = 1024;
|
||||
const WASTEFUL_THRESHOLD_AS_RATIO = 0.1;
|
||||
const IGNORE_THRESHOLD_IN_BYTES = 2048;
|
||||
const WASTEFUL_THRESHOLD_IN_BYTES = 25 * 1024;
|
||||
|
||||
class UsesResponsiveImages extends Audit {
|
||||
/**
|
||||
|
@ -39,7 +38,7 @@ class UsesResponsiveImages extends Audit {
|
|||
return {
|
||||
category: 'Images',
|
||||
name: 'uses-responsive-images',
|
||||
description: 'Has appropriately sized images',
|
||||
description: 'Avoids oversized images',
|
||||
helpText:
|
||||
'Image sizes served should be based on the device display size to save network bytes. ' +
|
||||
'Learn more about [responsive images](https://developers.google.com/web/fundamentals/design-and-ui/media/images) ' +
|
||||
|
@ -56,54 +55,41 @@ class UsesResponsiveImages extends Audit {
|
|||
static computeWaste(image, DPR) {
|
||||
const url = URL.getDisplayName(image.src);
|
||||
const actualPixels = image.naturalWidth * image.naturalHeight;
|
||||
const usedPixels = image.clientWidth * image.clientHeight;
|
||||
const usedPixelsFullDPR = usedPixels * Math.pow(DPR, 2);
|
||||
const usedPixels = image.clientWidth * image.clientHeight * Math.pow(DPR, 2);
|
||||
const wastedRatio = 1 - (usedPixels / actualPixels);
|
||||
const wastedRatioFullDPR = 1 - (usedPixelsFullDPR / actualPixels);
|
||||
|
||||
if (!Number.isFinite(wastedRatio)) {
|
||||
return new Error(`Invalid image sizing information ${url}`);
|
||||
} else if (wastedRatio <= 0) {
|
||||
// Image did not have sufficient resolution to fill display at DPR=1
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalBytes = image.networkRecord.resourceSize;
|
||||
const wastedBytes = Math.round(totalBytes * wastedRatio);
|
||||
|
||||
if (!Number.isFinite(wastedRatio)) {
|
||||
return new Error(`Invalid image sizing information ${url}`);
|
||||
} else if (wastedRatio <= 0 || wastedBytes < IGNORE_THRESHOLD_IN_BYTES) {
|
||||
// Image did not have sufficient resolution to fill display size
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
wastedBytes,
|
||||
isWasteful: wastedRatioFullDPR > WASTEFUL_THRESHOLD_AS_RATIO,
|
||||
result: {
|
||||
url,
|
||||
totalKb: Math.round(totalBytes / KB_IN_BYTES) + ' KB',
|
||||
potentialSavings: Math.round(100 * wastedRatio) + '%'
|
||||
url,
|
||||
preview: {
|
||||
url: image.networkRecord.url,
|
||||
mimeType: image.networkRecord.mimeType
|
||||
},
|
||||
totalBytes,
|
||||
wastedBytes,
|
||||
wastedPercent: 100 * wastedRatio,
|
||||
isWasteful: wastedBytes > WASTEFUL_THRESHOLD_IN_BYTES,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @return {!AuditResult}
|
||||
* @return {{results: !Array<Object>, tableHeadings: Object,
|
||||
* passes: boolean=, debugString: string=}}
|
||||
*/
|
||||
static audit(artifacts) {
|
||||
const networkRecords = artifacts.networkRecords[Audit.DEFAULT_PASS];
|
||||
return artifacts.requestNetworkThroughput(networkRecords).then(networkThroughput => {
|
||||
return UsesResponsiveImages.audit_(artifacts, networkThroughput);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @param {number} networkThroughput
|
||||
* @return {!AuditResult}
|
||||
*/
|
||||
static audit_(artifacts, networkThroughput) {
|
||||
static audit_(artifacts) {
|
||||
const images = artifacts.ImageUsage;
|
||||
const contentWidth = artifacts.ContentWidth;
|
||||
|
||||
let debugString;
|
||||
let totalWastedBytes = 0;
|
||||
let hasWastefulImage = false;
|
||||
const DPR = contentWidth.devicePixelRatio;
|
||||
const results = images.reduce((results, image) => {
|
||||
|
@ -120,43 +106,21 @@ class UsesResponsiveImages extends Audit {
|
|||
}
|
||||
|
||||
hasWastefulImage = hasWastefulImage || processed.isWasteful;
|
||||
totalWastedBytes += processed.wastedBytes;
|
||||
|
||||
results.push(Object.assign({
|
||||
preview: {
|
||||
url: image.networkRecord.url,
|
||||
mimeType: image.networkRecord.mimeType
|
||||
}
|
||||
}, processed.result));
|
||||
|
||||
results.push(processed);
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
let displayValue;
|
||||
if (results.length) {
|
||||
const totalWastedKB = Math.round(totalWastedBytes / KB_IN_BYTES);
|
||||
// Only round to nearest 10ms since we're relatively hand-wavy
|
||||
const totalWastedMs = Math.round(totalWastedBytes / networkThroughput * 100) * 10;
|
||||
displayValue = `${totalWastedKB}KB (~${totalWastedMs}ms) potential savings`;
|
||||
}
|
||||
|
||||
return UsesResponsiveImages.generateAuditResult({
|
||||
return {
|
||||
debugString,
|
||||
displayValue,
|
||||
rawValue: !hasWastefulImage,
|
||||
extendedInfo: {
|
||||
formatter: Formatter.SUPPORTED_FORMATS.TABLE,
|
||||
value: {
|
||||
results,
|
||||
tableHeadings: {
|
||||
preview: '',
|
||||
url: 'URL',
|
||||
totalKb: 'Original (KB)',
|
||||
potentialSavings: 'Potential Savings (%)'
|
||||
}
|
||||
}
|
||||
passes: !hasWastefulImage,
|
||||
results,
|
||||
tableHeadings: {
|
||||
preview: '',
|
||||
url: 'URL',
|
||||
totalKb: 'Original',
|
||||
potentialSavings: 'Potential Savings',
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +72,6 @@
|
|||
"manifest-short-name-length",
|
||||
"manifest-start-url",
|
||||
"theme-color-meta",
|
||||
"unused-css-rules",
|
||||
"content-width",
|
||||
"deprecations",
|
||||
"accessibility/aria-allowed-attr",
|
||||
|
@ -83,6 +82,9 @@
|
|||
"accessibility/image-alt",
|
||||
"accessibility/label",
|
||||
"accessibility/tabindex",
|
||||
"byte-efficiency/unused-css-rules",
|
||||
"byte-efficiency/uses-optimized-images",
|
||||
"byte-efficiency/uses-responsive-images",
|
||||
"dobetterweb/external-anchors-use-rel-noopener",
|
||||
"dobetterweb/appcache-manifest",
|
||||
"dobetterweb/geolocation-on-start",
|
||||
|
@ -96,8 +98,6 @@
|
|||
"dobetterweb/notification-on-start",
|
||||
"dobetterweb/script-blocking-first-paint",
|
||||
"dobetterweb/uses-http2",
|
||||
"dobetterweb/uses-optimized-images",
|
||||
"dobetterweb/uses-responsive-images",
|
||||
"dobetterweb/uses-passive-event-listeners"
|
||||
],
|
||||
|
||||
|
|
|
@ -28,11 +28,11 @@
|
|||
"user-timings",
|
||||
"screenshots",
|
||||
"critical-request-chains",
|
||||
"unused-css-rules",
|
||||
"byte-efficiency/unused-css-rules",
|
||||
"byte-efficiency/uses-optimized-images",
|
||||
"byte-efficiency/uses-responsive-images",
|
||||
"dobetterweb/link-blocking-first-paint",
|
||||
"dobetterweb/script-blocking-first-paint",
|
||||
"dobetterweb/uses-optimized-images",
|
||||
"dobetterweb/uses-responsive-images"
|
||||
"dobetterweb/script-blocking-first-paint"
|
||||
],
|
||||
|
||||
"aggregations": [{
|
||||
|
|
|
@ -28,6 +28,11 @@
|
|||
.table_list tr:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.table_list em {
|
||||
color: #999;
|
||||
font-style: normal;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.table_list code, .table_list pre {
|
||||
white-space: pre;
|
||||
font-family: monospace;
|
||||
|
|
|
@ -207,14 +207,22 @@ class Runner {
|
|||
* @return {!Array<string>}
|
||||
*/
|
||||
static getAuditList() {
|
||||
const ignoredFiles = [
|
||||
'audit.js',
|
||||
'accessibility/axe-audit.js',
|
||||
'byte-efficiency/byte-efficiency-audit.js'
|
||||
];
|
||||
|
||||
const fileList = [
|
||||
...fs.readdirSync(path.join(__dirname, './audits')),
|
||||
...fs.readdirSync(path.join(__dirname, './audits/dobetterweb')).map(f => `dobetterweb/${f}`),
|
||||
...fs.readdirSync(path.join(__dirname, './audits/accessibility'))
|
||||
.map(f => `accessibility/${f}`)
|
||||
.map(f => `accessibility/${f}`),
|
||||
...fs.readdirSync(path.join(__dirname, './audits/byte-efficiency'))
|
||||
.map(f => `byte-efficiency/${f}`)
|
||||
];
|
||||
return fileList.filter(f => {
|
||||
return /\.js$/.test(f) && f !== 'audit.js' && f !== 'accessibility/axe-audit.js';
|
||||
return /\.js$/.test(f) && !ignoredFiles.includes(f);
|
||||
}).sort();
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
const UnusedCSSAudit = require('../../audits/unused-css-rules.js');
|
||||
const UnusedCSSAudit = require('../../../audits/byte-efficiency/unused-css-rules.js');
|
||||
const assert = require('assert');
|
||||
|
||||
/* eslint-env mocha */
|
||||
|
@ -114,9 +114,9 @@ describe('Best Practices: unused css rules audit', () => {
|
|||
});
|
||||
|
||||
it('correctly computes potentialSavings', () => {
|
||||
assert.ok(map({used: [], unused: [1, 2]}).potentialSavings, '100%');
|
||||
assert.ok(map({used: [1, 2], unused: [1, 2]}).potentialSavings, '50%');
|
||||
assert.ok(map({used: [1, 2], unused: []}).potentialSavings, '0%');
|
||||
assert.equal(map({used: [], unused: [1, 2]}).wastedPercent, 100);
|
||||
assert.equal(map({used: [1, 2], unused: [1, 2]}).wastedPercent, 50);
|
||||
assert.equal(map({used: [1, 2], unused: []}).wastedPercent, 0);
|
||||
});
|
||||
|
||||
it('correctly computes url', () => {
|
||||
|
@ -149,10 +149,10 @@ describe('Best Practices: unused css rules audit', () => {
|
|||
Styles: []
|
||||
});
|
||||
|
||||
assert.equal(result.rawValue, true);
|
||||
assert.equal(result.results.length, 0);
|
||||
});
|
||||
|
||||
it('passes when rules are used', () => {
|
||||
it('ignores stylesheets that are 100% used', () => {
|
||||
const result = UnusedCSSAudit.audit_({
|
||||
networkRecords,
|
||||
URL: {finalUrl: ''},
|
||||
|
@ -173,16 +173,10 @@ describe('Best Practices: unused css rules audit', () => {
|
|||
]
|
||||
});
|
||||
|
||||
assert.ok(!result.displayValue);
|
||||
assert.equal(result.rawValue, true);
|
||||
assert.equal(result.extendedInfo.value.results.length, 2);
|
||||
assert.equal(result.extendedInfo.value.results[0].totalKb, '10 KB');
|
||||
assert.equal(result.extendedInfo.value.results[1].totalKb, '0 KB');
|
||||
assert.equal(result.extendedInfo.value.results[0].potentialSavings, '0%');
|
||||
assert.equal(result.extendedInfo.value.results[1].potentialSavings, '0%');
|
||||
assert.equal(result.results.length, 0);
|
||||
});
|
||||
|
||||
it('fails when rules are unused', () => {
|
||||
it('fails when lots of rules are unused', () => {
|
||||
const result = UnusedCSSAudit.audit_({
|
||||
networkRecords,
|
||||
URL: {finalUrl: ''},
|
||||
|
@ -190,6 +184,7 @@ describe('Best Practices: unused css rules audit', () => {
|
|||
{styleSheetId: 'a', used: true},
|
||||
{styleSheetId: 'a', used: false},
|
||||
{styleSheetId: 'a', used: false},
|
||||
{styleSheetId: 'a', used: false},
|
||||
{styleSheetId: 'b', used: true},
|
||||
{styleSheetId: 'b', used: false},
|
||||
{styleSheetId: 'c', used: false},
|
||||
|
@ -201,24 +196,20 @@ describe('Best Practices: unused css rules audit', () => {
|
|||
},
|
||||
{
|
||||
header: {styleSheetId: 'b', sourceURL: 'file://b.css'},
|
||||
content: `.my.favorite.selector { ${generate('rule: a; ', 1000)}; }`
|
||||
content: `${generate('123', 2050)}`
|
||||
},
|
||||
{
|
||||
header: {styleSheetId: 'c', sourceURL: ''},
|
||||
content: '.my.other.selector { rule: content; }'
|
||||
content: `${generate('123', 450)}` // will be filtered out
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
assert.ok(result.displayValue);
|
||||
assert.equal(result.rawValue, false);
|
||||
assert.equal(result.extendedInfo.value.results.length, 3);
|
||||
assert.equal(result.extendedInfo.value.results[0].totalKb, '10 KB');
|
||||
assert.equal(result.extendedInfo.value.results[1].totalKb, '3 KB');
|
||||
assert.equal(result.extendedInfo.value.results[2].totalKb, '0 KB');
|
||||
assert.equal(result.extendedInfo.value.results[0].potentialSavings, '67%');
|
||||
assert.equal(result.extendedInfo.value.results[1].potentialSavings, '50%');
|
||||
assert.equal(result.extendedInfo.value.results[2].potentialSavings, '100%');
|
||||
assert.equal(result.results.length, 2);
|
||||
assert.equal(result.results[0].totalBytes, 10 * 1024);
|
||||
assert.equal(result.results[1].totalBytes, 2050);
|
||||
assert.equal(result.results[0].wastedPercent, 75);
|
||||
assert.equal(result.results[1].wastedPercent, 50);
|
||||
});
|
||||
|
||||
it('does not include duplicate sheets', () => {
|
||||
|
@ -228,6 +219,7 @@ describe('Best Practices: unused css rules audit', () => {
|
|||
CSSUsage: [
|
||||
{styleSheetId: 'a', used: true},
|
||||
{styleSheetId: 'a', used: true},
|
||||
{styleSheetId: 'a', used: false},
|
||||
{styleSheetId: 'b', used: false},
|
||||
],
|
||||
Styles: [
|
||||
|
@ -243,28 +235,29 @@ describe('Best Practices: unused css rules audit', () => {
|
|||
]
|
||||
});
|
||||
|
||||
assert.ok(!result.displayValue);
|
||||
assert.equal(result.rawValue, true);
|
||||
assert.equal(result.extendedInfo.value.results.length, 1);
|
||||
assert.equal(result.results.length, 1);
|
||||
});
|
||||
|
||||
it('does not include empty sheets', () => {
|
||||
it('does not include empty or small sheets', () => {
|
||||
const result = UnusedCSSAudit.audit_({
|
||||
networkRecords,
|
||||
URL: {finalUrl: ''},
|
||||
CSSUsage: [
|
||||
{styleSheetId: 'a', used: true},
|
||||
{styleSheetId: 'a', used: true},
|
||||
{styleSheetId: 'a', used: false},
|
||||
{styleSheetId: 'b', used: true},
|
||||
{styleSheetId: 'b', used: false},
|
||||
{styleSheetId: 'b', used: false},
|
||||
],
|
||||
Styles: [
|
||||
{
|
||||
header: {styleSheetId: 'a', sourceURL: 'file://a.css'},
|
||||
content: '.my.selector {color: #ccc;}\n a {color: #fff}'
|
||||
content: `${generate('123', 4000)}`
|
||||
},
|
||||
{
|
||||
header: {styleSheetId: 'b', sourceURL: 'file://b.css'},
|
||||
content: '.my.favorite.selector { rule: content; }'
|
||||
content: `${generate('123', 500)}`
|
||||
},
|
||||
{
|
||||
header: {styleSheetId: 'c', sourceURL: 'c.css'},
|
||||
|
@ -281,9 +274,8 @@ describe('Best Practices: unused css rules audit', () => {
|
|||
]
|
||||
});
|
||||
|
||||
assert.ok(!result.displayValue);
|
||||
assert.equal(result.rawValue, true);
|
||||
assert.equal(result.extendedInfo.value.results.length, 2);
|
||||
assert.equal(result.results.length, 1);
|
||||
assert.equal(result.results[0].numUnused, 1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,7 +15,8 @@
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
const UsesOptimizedImagesAudit = require('../../../audits/dobetterweb/uses-optimized-images.js');
|
||||
const UsesOptimizedImagesAudit =
|
||||
require('../../../audits/byte-efficiency/uses-optimized-images.js');
|
||||
const assert = require('assert');
|
||||
|
||||
function generateImage(type, originalSize, webpSize, jpegSize) {
|
||||
|
@ -37,43 +38,61 @@ function generateImage(type, originalSize, webpSize, jpegSize) {
|
|||
/* eslint-env mocha */
|
||||
|
||||
describe('Page uses optimized images', () => {
|
||||
it('fails when one jpeg image is unoptimized', () => {
|
||||
it('passes when there is only insignificant savings', () => {
|
||||
const auditResult = UsesOptimizedImagesAudit.audit_({
|
||||
OptimizedImages: [
|
||||
generateImage('jpeg', 5000, 4000, 4500),
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(auditResult.rawValue, false);
|
||||
assert.equal(auditResult.passes, true);
|
||||
assert.equal(auditResult.results.length, 0);
|
||||
});
|
||||
|
||||
const headings = auditResult.extendedInfo.value.tableHeadings;
|
||||
it('passes with warning when there is only small savings', () => {
|
||||
const auditResult = UsesOptimizedImagesAudit.audit_({
|
||||
OptimizedImages: [
|
||||
generateImage('jpeg', 15000, 4000, 4500),
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(auditResult.passes, true);
|
||||
assert.equal(auditResult.results.length, 1);
|
||||
});
|
||||
|
||||
it('fails when one jpeg image is unoptimized', () => {
|
||||
const auditResult = UsesOptimizedImagesAudit.audit_({
|
||||
OptimizedImages: [
|
||||
generateImage('jpeg', 71000, 40000, 45000),
|
||||
],
|
||||
});
|
||||
|
||||
const headings = auditResult.tableHeadings;
|
||||
assert.equal(auditResult.passes, false);
|
||||
assert.deepEqual(Object.keys(headings).map(key => headings[key]),
|
||||
['', 'URL', 'Original (KB)', 'WebP Savings (%)', 'JPEG Savings (%)'],
|
||||
'table headings are correct and in order');
|
||||
['', 'URL', 'Original', 'WebP Savings', 'JPEG Savings'],
|
||||
'table headings are correct and in order');
|
||||
});
|
||||
|
||||
it('fails when one png image is highly unoptimized', () => {
|
||||
const auditResult = UsesOptimizedImagesAudit.audit_({
|
||||
OptimizedImages: [
|
||||
generateImage('png', 100000, 40000),
|
||||
generateImage('png', 150000, 40000),
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(auditResult.rawValue, false);
|
||||
assert.equal(auditResult.passes, false);
|
||||
});
|
||||
|
||||
it('fails when images are collectively unoptimized', () => {
|
||||
const auditResult = UsesOptimizedImagesAudit.audit_({
|
||||
OptimizedImages: [
|
||||
generateImage('png', 50000, 30000),
|
||||
generateImage('jpeg', 50000, 30000, 40000),
|
||||
generateImage('png', 50000, 30000),
|
||||
generateImage('jpeg', 50000, 30000, 40000),
|
||||
generateImage('png', 50001, 30000),
|
||||
],
|
||||
});
|
||||
const OptimizedImages = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
OptimizedImages.push(generateImage('png', 100000, 10000));
|
||||
}
|
||||
|
||||
assert.equal(auditResult.rawValue, false);
|
||||
const auditResult = UsesOptimizedImagesAudit.audit_({OptimizedImages});
|
||||
assert.equal(auditResult.passes, false);
|
||||
assert.equal(auditResult.passes, false);
|
||||
});
|
||||
|
||||
it('passes when all images are sufficiently optimized', () => {
|
||||
|
@ -87,7 +106,7 @@ describe('Page uses optimized images', () => {
|
|||
],
|
||||
});
|
||||
|
||||
assert.equal(auditResult.rawValue, true);
|
||||
assert.equal(auditResult.passes, true);
|
||||
});
|
||||
|
||||
it('limits output of data URIs', () => {
|
||||
|
@ -96,7 +115,7 @@ describe('Page uses optimized images', () => {
|
|||
OptimizedImages: [image],
|
||||
});
|
||||
|
||||
const actualUrl = auditResult.extendedInfo.value.results[0].url;
|
||||
const actualUrl = auditResult.results[0].url;
|
||||
assert.ok(actualUrl.length < image.url.length, `${actualUrl} >= ${image.url}`);
|
||||
});
|
||||
|
|
@ -15,7 +15,8 @@
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
const UsesResponsiveImagesAudit = require('../../../audits/dobetterweb/uses-responsive-images.js');
|
||||
const UsesResponsiveImagesAudit =
|
||||
require('../../../audits/byte-efficiency/uses-responsive-images.js');
|
||||
const assert = require('assert');
|
||||
|
||||
/* eslint-env mocha */
|
||||
|
@ -41,43 +42,59 @@ function generateImage(clientSize, naturalSize, networkRecord, src = 'https://go
|
|||
}
|
||||
|
||||
describe('Page uses responsive images', () => {
|
||||
it('fails when an image is much larger than displayed size', () => {
|
||||
const auditResult = UsesResponsiveImagesAudit.audit_({
|
||||
ContentWidth: {devicePixelRatio: 1},
|
||||
ImageUsage: [
|
||||
generateImage(
|
||||
generateSize(100, 100),
|
||||
generateSize(200, 200, 'natural'),
|
||||
generateRecord(60, 250)
|
||||
),
|
||||
generateImage(
|
||||
generateSize(100, 100),
|
||||
generateSize(90, 90),
|
||||
generateRecord(30, 200)
|
||||
),
|
||||
],
|
||||
});
|
||||
function testImage(condition, data) {
|
||||
const description = `${data.passes ? 'passes' : 'fails'} when an image is ${condition}`;
|
||||
it(description, () => {
|
||||
const result = UsesResponsiveImagesAudit.audit_({
|
||||
ContentWidth: {devicePixelRatio: data.devicePixelRatio || 1},
|
||||
ImageUsage: [
|
||||
generateImage(
|
||||
generateSize(...data.clientSize),
|
||||
generateSize(...data.naturalSize, 'natural'),
|
||||
generateRecord(data.sizeInKb, data.durationInMs || 200)
|
||||
)
|
||||
]
|
||||
});
|
||||
|
||||
assert.equal(auditResult.rawValue, false);
|
||||
assert.equal(auditResult.extendedInfo.value.results.length, 1);
|
||||
assert.ok(/45KB/.test(auditResult.displayValue), 'computes total kb');
|
||||
assert.equal(result.passes, data.passes);
|
||||
assert.equal(result.results.length, data.listed || !data.passes ? 1 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
testImage('larger than displayed size', {
|
||||
passes: false,
|
||||
listed: false,
|
||||
devicePixelRatio: 2,
|
||||
clientSize: [100, 100],
|
||||
naturalSize: [300, 300],
|
||||
sizeInKb: 200
|
||||
});
|
||||
|
||||
it('fails when an image is much larger than DPR displayed size', () => {
|
||||
const auditResult = UsesResponsiveImagesAudit.audit_({
|
||||
ContentWidth: {devicePixelRatio: 2},
|
||||
ImageUsage: [
|
||||
generateImage(
|
||||
generateSize(100, 100),
|
||||
generateSize(300, 300, 'natural'),
|
||||
generateRecord(90, 500)
|
||||
),
|
||||
],
|
||||
});
|
||||
testImage('smaller than displayed size', {
|
||||
passes: true,
|
||||
listed: false,
|
||||
devicePixelRatio: 2,
|
||||
clientSize: [200, 200],
|
||||
naturalSize: [300, 300],
|
||||
sizeInKb: 200
|
||||
});
|
||||
|
||||
assert.equal(auditResult.rawValue, false);
|
||||
assert.equal(auditResult.extendedInfo.value.results.length, 1);
|
||||
assert.ok(/80KB/.test(auditResult.displayValue), 'compute total kb');
|
||||
testImage('small in file size', {
|
||||
passes: true,
|
||||
listed: true,
|
||||
devicePixelRatio: 2,
|
||||
clientSize: [100, 100],
|
||||
naturalSize: [300, 300],
|
||||
sizeInKb: 10
|
||||
});
|
||||
|
||||
testImage('very small in file size', {
|
||||
passes: true,
|
||||
listed: false,
|
||||
devicePixelRatio: 2,
|
||||
clientSize: [100, 100],
|
||||
naturalSize: [300, 300],
|
||||
sizeInKb: 1
|
||||
});
|
||||
|
||||
it('handles images without network record', () => {
|
||||
|
@ -92,8 +109,8 @@ describe('Page uses responsive images', () => {
|
|||
],
|
||||
});
|
||||
|
||||
assert.equal(auditResult.rawValue, true);
|
||||
assert.equal(auditResult.extendedInfo.value.results.length, 0);
|
||||
assert.equal(auditResult.passes, true);
|
||||
assert.equal(auditResult.results.length, 0);
|
||||
});
|
||||
|
||||
it('passes when all images are not wasteful', () => {
|
||||
|
@ -102,7 +119,7 @@ describe('Page uses responsive images', () => {
|
|||
ImageUsage: [
|
||||
generateImage(
|
||||
generateSize(200, 200),
|
||||
generateSize(210, 210, 'natural'),
|
||||
generateSize(450, 450, 'natural'),
|
||||
generateRecord(100, 300)
|
||||
),
|
||||
generateImage(
|
||||
|
@ -119,7 +136,7 @@ describe('Page uses responsive images', () => {
|
|||
],
|
||||
});
|
||||
|
||||
assert.equal(auditResult.rawValue, true);
|
||||
assert.equal(auditResult.extendedInfo.value.results.length, 2);
|
||||
assert.equal(auditResult.passes, true);
|
||||
assert.equal(auditResult.results.length, 2);
|
||||
});
|
||||
});
|
|
@ -20,7 +20,7 @@
|
|||
"build-extension": "cd ./lighthouse-extension && npm run build",
|
||||
"build-viewer": "cd ./lighthouse-viewer && npm run build",
|
||||
"lint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .",
|
||||
"smoke": "bash lighthouse-cli/test/smokehouse/offline-local/run-tests.sh && bash lighthouse-cli/test/smokehouse/dobetterweb/run-tests.sh",
|
||||
"smoke": "bash lighthouse-cli/test/smokehouse/offline-local/run-tests.sh && bash lighthouse-cli/test/smokehouse/dobetterweb/run-tests.sh && bash lighthouse-cli/test/smokehouse/byte-efficiency/run-tests.sh",
|
||||
"coverage": "node $(npm bin)/istanbul cover -x \"**/third_party/**\" _mocha -- $(find */test -name '*-test.js') --timeout 10000 --reporter progress",
|
||||
"coveralls": "npm run coverage && cat ./coverage/lcov.info | coveralls",
|
||||
"start": "node ./lighthouse-cli/index.js",
|
||||
|
|
Загрузка…
Ссылка в новой задаче