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:
Patrick Hulce 2017-02-14 16:43:16 -08:00 коммит произвёл Paul Irish
Родитель e72aadccf5
Коммит d1b91101a8
22 изменённых файлов: 499 добавлений и 385 удалений

Двоичные данные
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

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.0 KiB

Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-480x320.jpg поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 18 KiB

Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-480x320.webp поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 11 KiB

Двоичные данные
lighthouse-cli/test/fixtures/byte-efficiency/lighthouse-unoptimized.jpg поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 51 KiB

87
lighthouse-cli/test/fixtures/byte-efficiency/tester.html поставляемый Normal file
Просмотреть файл

@ -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",