diff --git a/.eslintignore b/.eslintignore
index 1f2d7ebda64f..cd661a4b358f 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -7,6 +7,8 @@
**/generated/**
**/source-maps/**
+lighthouse-cli/test/fixtures/byte-efficiency/bundle.js
+
/dist/**
coverage/**
diff --git a/.eslintrc.js b/.eslintrc.js
index 6d28a329d05b..f8b2e01e1379 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -50,7 +50,7 @@ module.exports = {
'no-unused-vars': [2, {
vars: 'all',
args: 'after-used',
- argsIgnorePattern: '(^reject$|^_$)',
+ argsIgnorePattern: '(^reject$|^_+$)',
varsIgnorePattern: '(^_$)',
}],
'space-infix-ops': 2,
diff --git a/lighthouse-cli/test/fixtures/byte-efficiency/bundle.js b/lighthouse-cli/test/fixtures/byte-efficiency/bundle.js
new file mode 100644
index 000000000000..f63e3158beb0
--- /dev/null
+++ b/lighthouse-cli/test/fixtures/byte-efficiency/bundle.js
@@ -0,0 +1,156 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __webpack_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+const a = __webpack_require__(1);
+const b = __webpack_require__(2);
+const c = __webpack_require__(3);
+
+if (true) {
+ a();
+ b.half();
+} else {}
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports) {
+
+module.exports = function () {
+ const kb2 = '................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................';
+ let i;
+ for (i = 0; i < 100; i++) {
+ i = i;
+ }
+ return i;
+}
+
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports) {
+
+function half() {
+ const kb2 = '................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................';
+ let i;
+ for (i = 0; i < 100; i++) {
+ i = i;
+ }
+ return i;
+}
+
+function otherHalf() {
+ const kb2 = '................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................';
+ let i;
+ for (i = 0; i < 100; i++) {
+ i = i;
+ }
+ return i;
+}
+
+module.exports = { half, otherHalf };
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports) {
+
+module.exports = function () {
+ const kb2 = '................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................';
+ let i;
+ for (i = 0; i < 100; i++) {
+ i = i;
+ }
+ return i;
+}
+
+
+/***/ })
+/******/ ]);
+//# sourceMappingURL=bundle.js.map
\ No newline at end of file
diff --git a/lighthouse-cli/test/fixtures/byte-efficiency/bundle.js.map b/lighthouse-cli/test/fixtures/byte-efficiency/bundle.js.map
new file mode 100644
index 000000000000..bafb42a6396c
--- /dev/null
+++ b/lighthouse-cli/test/fixtures/byte-efficiency/bundle.js.map
@@ -0,0 +1,13 @@
+{
+ "version": 3,
+ "sources": [
+ "webpack:///webpack/bootstrap",
+ "webpack:///./entry.js",
+ "webpack:///./a.js",
+ "webpack:///./b.js",
+ "webpack:///./c.js"
+ ],
+ "names": [],
+ "mappings": ";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;AClFA,UAAU,mBAAO,CAAC,CAAQ;AAC1B,UAAU,mBAAO,CAAC,CAAQ;AAC1B,UAAU,mBAAO,CAAC,CAAQ;;AAE1B,IAAI,IAAW;AACf;AACA;AACA,CAAC,MAAM,EAEN;;;;;;;ACTD;AACA;AACA;AACA,aAAa,SAAS;AACtB;AACA;AACA;AACA;;;;;;;ACPA;AACA;AACA;AACA,aAAa,SAAS;AACtB;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,aAAa,SAAS;AACtB;AACA;AACA;AACA;;AAEA,kBAAkB;;;;;;;AClBlB;AACA;AACA;AACA,aAAa,SAAS;AACtB;AACA;AACA;AACA",
+ "file": "bundle.js"
+}
\ No newline at end of file
diff --git a/lighthouse-cli/test/fixtures/byte-efficiency/tester.html b/lighthouse-cli/test/fixtures/byte-efficiency/tester.html
index e1368d5a63f1..51f3f5852afb 100644
--- a/lighthouse-cli/test/fixtures/byte-efficiency/tester.html
+++ b/lighthouse-cli/test/fixtures/byte-efficiency/tester.html
@@ -54,6 +54,7 @@
}
+
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/byte-efficiency/byte-config.js b/lighthouse-cli/test/smokehouse/test-definitions/byte-efficiency/byte-config.js
index 47e7ab85b046..a58a9084ae5e 100644
--- a/lighthouse-cli/test/smokehouse/test-definitions/byte-efficiency/byte-config.js
+++ b/lighthouse-cli/test/smokehouse/test-definitions/byte-efficiency/byte-config.js
@@ -27,6 +27,13 @@ const config = {
],
throttlingMethod: 'devtools',
},
+ // source-maps is not yet in the default config.
+ passes: [{
+ passName: 'defaultPass',
+ gatherers: [
+ 'source-maps',
+ ],
+ }],
};
module.exports = config;
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/byte-efficiency/expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/byte-efficiency/expectations.js
index 28d3c8e9f0ec..3978ff7a3d99 100644
--- a/lighthouse-cli/test/smokehouse/test-definitions/byte-efficiency/expectations.js
+++ b/lighthouse-cli/test/smokehouse/test-definitions/byte-efficiency/expectations.js
@@ -29,6 +29,14 @@ const expectations = [
source: 'head',
devtoolsNodePath: '2,HTML,0,HEAD,5,SCRIPT',
},
+ {
+ type: null,
+ src: 'http://localhost:10200/byte-efficiency/bundle.js',
+ async: false,
+ defer: false,
+ source: 'head',
+ devtoolsNodePath: '2,HTML,0,HEAD,6,SCRIPT',
+ },
{
type: null,
src: null,
@@ -106,6 +114,12 @@ const expectations = [
wastedBytes: '6559 +/- 100',
wastedPercent: 100,
},
+ {
+ url: 'http://localhost:10200/byte-efficiency/bundle.js',
+ totalBytes: '13000 +/- 1000',
+ wastedBytes: '2350 +/- 100',
+ wastedPercent: '19 +/- 5',
+ },
],
},
},
@@ -122,9 +136,38 @@ const expectations = [
details: {
overallSavingsBytes: '>=25000',
overallSavingsMs: '>300',
- items: {
- length: 2,
- },
+ items: [
+ {
+ url: 'http://localhost:10200/byte-efficiency/script.js',
+ totalBytes: '53000 +/- 1000',
+ wastedBytes: '22000 +/- 1000',
+ },
+ {
+ url: 'http://localhost:10200/byte-efficiency/tester.html',
+ totalBytes: '15000 +/- 1000',
+ wastedBytes: '6500 +/- 1000',
+ },
+ {
+ url: 'http://localhost:10200/byte-efficiency/bundle.js',
+ totalBytes: 12913,
+ wastedBytes: 5827,
+ sources: [
+ '…./b.js',
+ '…./c.js',
+ '…webpack/bootstrap',
+ ],
+ sourceBytes: [
+ 4417,
+ 2200,
+ 2809,
+ ],
+ sourceWastedBytes: [
+ 2191,
+ 2182,
+ 1259,
+ ],
+ },
+ ],
},
},
'offscreen-images': {
@@ -156,7 +199,7 @@ const expectations = [
overallSavingsMs: '>700',
overallSavingsBytes: '>50000',
items: {
- length: 2,
+ length: 3,
},
},
},
diff --git a/lighthouse-core/audits/byte-efficiency/unused-javascript.js b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
index 305d77469a35..1ff94e71eaae 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-javascript.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
@@ -6,6 +6,7 @@
'use strict';
const ByteEfficiencyAudit = require('./byte-efficiency-audit.js');
+const JSBundles = require('../../computed/js-bundles.js');
const i18n = require('../../lib/i18n/i18n.js');
const UIStrings = {
@@ -19,6 +20,41 @@ const UIStrings = {
const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
const IGNORE_THRESHOLD_IN_BYTES = 2048;
+const IGNORE_BUNDLE_SOURCE_THRESHOLD_IN_BYTES = 512;
+
+/**
+ * @param {string[]} strings
+ */
+function commonPrefix(strings) {
+ if (!strings.length) {
+ return '';
+ }
+
+ const maxWord = strings.reduce((a, b) => a > b ? a : b);
+ let prefix = strings.reduce((a, b) => a > b ? b : a);
+ while (!maxWord.startsWith(prefix)) {
+ prefix = prefix.slice(0, -1);
+ }
+
+ return prefix;
+}
+
+/**
+ * @param {string[]} strings
+ * @param {string} commonPrefix
+ * @return {string[]}
+ */
+function trimCommonPrefix(strings, commonPrefix) {
+ if (!commonPrefix) return strings;
+ return strings.map(s => s.startsWith(commonPrefix) ? '…' + s.slice(commonPrefix.length) : s);
+}
+
+/**
+ * @typedef WasteData
+ * @property {Uint8Array} unusedByIndex
+ * @property {number} unusedLength
+ * @property {number} contentLength
+ */
class UnusedJavaScript extends ByteEfficiencyAudit {
/**
@@ -30,26 +66,25 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
title: str_(UIStrings.title),
description: str_(UIStrings.description),
scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.NUMERIC,
- requiredArtifacts: ['JsUsage', 'devtoolsLogs', 'traces'],
+ requiredArtifacts: ['JsUsage', 'ScriptElements', 'devtoolsLogs', 'traces'],
+ __internalOptionalArtifacts: ['SourceMaps'],
};
}
/**
- * @param {LH.Crdp.Profiler.ScriptCoverage} script
- * @return {{unusedLength: number, contentLength: number}}
+ * @param {LH.Crdp.Profiler.ScriptCoverage} scriptCoverage
+ * @return {WasteData}
*/
- static computeWaste(script) {
+ static computeWaste(scriptCoverage) {
let maximumEndOffset = 0;
- for (const func of script.functions) {
- for (const range of func.ranges) {
- maximumEndOffset = Math.max(maximumEndOffset, range.endOffset);
- }
+ for (const func of scriptCoverage.functions) {
+ maximumEndOffset = Math.max(maximumEndOffset, ...func.ranges.map(r => r.endOffset));
}
// We only care about unused ranges of the script, so we can ignore all the nesting and safely
// assume that if a range is unexecuted, all nested ranges within it will also be unexecuted.
const unusedByIndex = new Uint8Array(maximumEndOffset);
- for (const func of script.functions) {
+ for (const func of scriptCoverage.functions) {
for (const range of func.ranges) {
if (range.count === 0) {
for (let i = range.startOffset; i < range.endOffset; i++) {
@@ -65,43 +100,133 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
}
return {
+ unusedByIndex,
unusedLength: unused,
contentLength: maximumEndOffset,
};
}
/**
- * @param {Array<{unusedLength: number, contentLength: number}>} wasteData
- * @param {LH.Artifacts.NetworkRequest} networkRecord
+ * @param {LH.Audit.ByteEfficiencyItem} item
+ * @param {WasteData[]} wasteData
+ * @param {LH.Artifacts.Bundle} bundle
+ * @param {ReturnType} lengths
+ * @param {number} bundleSourceUnusedThreshold
+ */
+ static createBundleMultiData(item, wasteData, bundle, lengths, bundleSourceUnusedThreshold) {
+ if (!bundle.script.content) return;
+
+ /** @type {Record} */
+ const files = {};
+
+ const lineLengths = bundle.script.content.split('\n').map(l => l.length);
+ let totalSoFar = 0;
+ const lineOffsets = lineLengths.map(len => {
+ const retVal = totalSoFar;
+ totalSoFar += len + 1;
+ return retVal;
+ });
+
+ // @ts-ignore: We will upstream computeLastGeneratedColumns to CDT eventually.
+ bundle.map.computeLastGeneratedColumns();
+ for (const mapping of bundle.map.mappings()) {
+ let offset = lineOffsets[mapping.lineNumber];
+
+ offset += mapping.columnNumber;
+ const lastColumnOfMapping =
+ // @ts-ignore: We will upstream lastColumnNumber to CDT eventually.
+ (mapping.lastColumnNumber - 1) || lineLengths[mapping.lineNumber];
+ for (let i = mapping.columnNumber; i <= lastColumnOfMapping; i++) {
+ if (wasteData.every(data => data.unusedByIndex[offset] === 1)) {
+ const key = mapping.sourceURL || '(unmapped)';
+ files[key] = (files[key] || 0) + 1;
+ }
+ offset += 1;
+ }
+ }
+
+ const transferRatio = lengths.transfer / lengths.content;
+ const topUnusedFilesSizes = Object.entries(files)
+ .filter(([_, unusedBytes]) => unusedBytes * transferRatio >= bundleSourceUnusedThreshold)
+ .sort(([_, unusedBytes1], [__, unusedBytes2]) => unusedBytes2 - unusedBytes1)
+ .slice(0, 5)
+ .map(([key, unusedBytes]) => {
+ const total = key === '(unmapped)' ? bundle.sizes.unmappedBytes : bundle.sizes.files[key];
+ return {
+ key,
+ unused: Math.round(unusedBytes * transferRatio),
+ total: Math.round(total * transferRatio),
+ };
+ });
+
+ const commonSourcePrefix = commonPrefix([...bundle.map._sourceInfos.keys()]);
+ Object.assign(item, {
+ sources: trimCommonPrefix(topUnusedFilesSizes.map(d => d.key), commonSourcePrefix),
+ sourceBytes: topUnusedFilesSizes.map(d => d.total),
+ sourceWastedBytes: topUnusedFilesSizes.map(d => d.unused),
+ });
+ }
+
+ /**
+ * @param {WasteData[]} wasteData
+ * @param {string} url
+ * @param {ReturnType} lengths
* @return {LH.Audit.ByteEfficiencyItem}
*/
- static mergeWaste(wasteData, networkRecord) {
- let unusedLength = 0;
- let contentLength = 0;
+ static mergeWaste(wasteData, url, lengths) {
+ let unused = 0;
+ let content = 0;
+ // TODO: this is right for multiple script tags in an HTML document,
+ // but may be wrong for multiple frames using the same script resource.
for (const usage of wasteData) {
- unusedLength += usage.unusedLength;
- contentLength += usage.contentLength;
+ unused += usage.unusedLength;
+ content += usage.contentLength;
}
- const totalBytes = ByteEfficiencyAudit.estimateTransferSize(networkRecord, contentLength,
- 'Script');
- const wastedRatio = (unusedLength / contentLength) || 0;
- const wastedBytes = Math.round(totalBytes * wastedRatio);
+ const wastedRatio = (unused / content) || 0;
+ const wastedBytes = Math.round(lengths.transfer * wastedRatio);
return {
- url: networkRecord.url,
- totalBytes,
+ url: url,
+ totalBytes: lengths.transfer,
wastedBytes,
wastedPercent: 100 * wastedRatio,
};
}
+ /**
+ * @param {WasteData[]} wasteData
+ * @param {LH.Artifacts.NetworkRequest} networkRecord
+ */
+ static determineLengths(wasteData, networkRecord) {
+ let unused = 0;
+ let content = 0;
+ // TODO: this is right for multiple script tags in an HTML document,
+ // but may be wrong for multiple frames using the same script resource.
+ for (const usage of wasteData) {
+ unused += usage.unusedLength;
+ content += usage.contentLength;
+ }
+ const transfer = ByteEfficiencyAudit.estimateTransferSize(networkRecord, content, 'Script');
+
+ return {
+ content,
+ unused,
+ transfer,
+ };
+ }
+
/**
* @param {LH.Artifacts} artifacts
* @param {Array} networkRecords
- * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
+ * @param {LH.Audit.Context} context
+ * @return {Promise}
*/
- static audit_(artifacts, networkRecords) {
+ static async audit_(artifacts, networkRecords, context) {
+ const bundles = artifacts.SourceMaps ? await JSBundles.request(artifacts, context) : [];
+ const {bundleSourceUnusedThreshold = IGNORE_BUNDLE_SOURCE_THRESHOLD_IN_BYTES} =
+ context.options || {};
+
/** @type {Map>} */
const scriptsByUrl = new Map();
for (const script of artifacts.JsUsage) {
@@ -111,21 +236,29 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
}
const items = [];
- for (const [url, scripts] of scriptsByUrl.entries()) {
+ for (const [url, scriptCoverage] of scriptsByUrl.entries()) {
const networkRecord = networkRecords.find(record => record.url === url);
if (!networkRecord) continue;
- const wasteData = scripts.map(UnusedJavaScript.computeWaste);
- const item = UnusedJavaScript.mergeWaste(wasteData, networkRecord);
+ const wasteData = scriptCoverage.map(UnusedJavaScript.computeWaste);
+ const lengths = UnusedJavaScript.determineLengths(wasteData, networkRecord);
+ const bundle = bundles.find(b => b.script.src === url);
+ const item = UnusedJavaScript.mergeWaste(wasteData, networkRecord.url, lengths);
if (item.wastedBytes <= IGNORE_THRESHOLD_IN_BYTES) continue;
+ if (bundle) {
+ UnusedJavaScript.createBundleMultiData(
+ item, wasteData, bundle, lengths, bundleSourceUnusedThreshold);
+ }
items.push(item);
}
return {
items,
headings: [
- {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
- {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
- {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
+ /* eslint-disable max-len */
+ {key: 'url', valueType: 'url', subRows: {key: 'sources', valueType: 'code'}, label: str_(i18n.UIStrings.columnURL)},
+ {key: 'totalBytes', valueType: 'bytes', subRows: {key: 'sourceBytes'}, label: str_(i18n.UIStrings.columnSize)},
+ {key: 'wastedBytes', valueType: 'bytes', subRows: {key: 'sourceWastedBytes'}, label: str_(i18n.UIStrings.columnWastedBytes)},
+ /* eslint-enable max-len */
],
};
}
diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index 582b7dcea100..1e54ca15675b 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -404,9 +404,10 @@ class DetailsRenderer {
label: '',
};
const values = row[subRowsHeading.key];
- if (!Array.isArray(values)) continue;
- const subRowsElement = this._renderSubRows(values, subRowsHeading);
- valueFragment.appendChild(subRowsElement);
+ if (Array.isArray(values)) {
+ const subRowsElement = this._renderSubRows(values, subRowsHeading);
+ valueFragment.appendChild(subRowsElement);
+ }
}
if (valueFragment.childElementCount) {
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 6e0858f63732..59e75e5a1717 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -313,6 +313,10 @@
display: none !important;
}
+.lh-root pre {
+ margin: 0;
+}
+
.lh-root details > summary {
cursor: pointer;
}
diff --git a/lighthouse-core/test/audits/byte-efficiency/unused-javascript-test.js b/lighthouse-core/test/audits/byte-efficiency/unused-javascript-test.js
index 931cc64aa31e..d4b45d6c04f9 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unused-javascript-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unused-javascript-test.js
@@ -5,18 +5,45 @@
*/
'use strict';
-const UnusedJavaScript = require('../../../audits/byte-efficiency/unused-javascript.js');
const assert = require('assert');
+const fs = require('fs');
+const UnusedJavaScript = require('../../../audits/byte-efficiency/unused-javascript.js');
+const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js');
+
+function load(name) {
+ const dir = `${__dirname}/../../fixtures/source-maps`;
+ const mapJson = fs.readFileSync(`${dir}/${name}.js.map`, 'utf-8');
+ const content = fs.readFileSync(`${dir}/${name}.js`, 'utf-8');
+ const usageJson = fs.readFileSync(`${dir}/${name}.usage.json`, 'utf-8');
+ const exportedUsage = JSON.parse(usageJson);
+
+ // Usage is exported from DevTools, which simplifies the real format of the
+ // usage protocol.
+ const usage = {
+ url: exportedUsage.url,
+ functions: [
+ {
+ ranges: exportedUsage.ranges.map((range, i) => {
+ return {
+ startOffset: range.start,
+ endOffset: range.end,
+ count: i % 2 === 0 ? 0 : 1,
+ };
+ }),
+ },
+ ],
+ };
+
+ return {map: JSON.parse(mapJson), content, usage};
+}
/* eslint-env jest */
function generateRecord(url, transferSize, resourceType) {
- url = `https://google.com/${url}`;
return {url, transferSize, resourceType};
}
-function generateScript(url, ranges, transferSize = 1000) {
- url = `https://google.com/${url}`;
+function generateUsage(url, ranges, transferSize = 1000) {
const functions = ranges.map(range => {
return {
ranges: [
@@ -35,21 +62,21 @@ function generateScript(url, ranges, transferSize = 1000) {
describe('UnusedJavaScript audit', () => {
describe('#computeWaste', () => {
it('should identify used', () => {
- const usage = generateScript('myscript.js', [[0, 100, true]]);
+ const usage = generateUsage('myscript.js', [[0, 100, true]]);
const result = UnusedJavaScript.computeWaste(usage);
assert.equal(result.unusedLength, 0);
assert.equal(result.contentLength, 100);
});
it('should identify unused', () => {
- const usage = generateScript('myscript.js', [[0, 100, false]]);
+ const usage = generateUsage('myscript.js', [[0, 100, false]]);
const result = UnusedJavaScript.computeWaste(usage);
assert.equal(result.unusedLength, 100);
assert.equal(result.contentLength, 100);
});
it('should identify nested unused', () => {
- const usage = generateScript('myscript.js', [
+ const usage = generateUsage('myscript.js', [
[0, 100, true], // 40% used overall
[0, 10, true],
@@ -71,21 +98,24 @@ describe('UnusedJavaScript audit', () => {
});
describe('audit_', () => {
- const scriptUnknown = generateScript('', [[0, 3000, false]]);
- const scriptA = generateScript('scriptA.js', [[0, 100, true]]);
- const scriptB = generateScript('scriptB.js', [[0, 200, true], [0, 50, false]]);
- const inlineA = generateScript('inline.html', [[0, 5000, true], [5000, 6000, false]]);
- const inlineB = generateScript('inline.html', [[0, 15000, true], [0, 5000, false]]);
- const recordA = generateRecord('scriptA.js', 35000, 'Script');
- const recordB = generateRecord('scriptB.js', 50000, 'Script');
- const recordInline = generateRecord('inline.html', 1000000, 'Document');
-
- const result = UnusedJavaScript.audit_(
- {JsUsage: [scriptA, scriptB, scriptUnknown, inlineA, inlineB]},
- [recordA, recordB, recordInline]
- );
-
- it('should merge duplicates', () => {
+ const domain = 'https://www.google.com';
+ const scriptUnknown = generateUsage(domain, [[0, 3000, false]]);
+ const scriptA = generateUsage(`${domain}/scriptA.js`, [[0, 100, true]]);
+ const scriptB = generateUsage(`${domain}/scriptB.js`, [[0, 200, true], [0, 50, false]]);
+ const inlineA = generateUsage(`${domain}/inline.html`, [[0, 5000, true], [5000, 6000, false]]);
+ const inlineB = generateUsage(`${domain}/inline.html`, [[0, 15000, true], [0, 5000, false]]);
+ const recordA = generateRecord(`${domain}/scriptA.js`, 35000, 'Script');
+ const recordB = generateRecord(`${domain}/scriptB.js`, 50000, 'Script');
+ const recordInline = generateRecord(`${domain}/inline.html`, 1000000, 'Document');
+
+ it('should merge duplicates', async () => {
+ const context = {computedCache: new Map()};
+ const networkRecords = [recordA, recordB, recordInline];
+ const artifacts = {
+ JsUsage: [scriptA, scriptB, scriptUnknown, inlineA, inlineB],
+ devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
+ };
+ const result = await UnusedJavaScript.audit_(artifacts, networkRecords, context);
assert.equal(result.items.length, 2);
const scriptBWaste = result.items[0];
@@ -98,5 +128,62 @@ describe('UnusedJavaScript audit', () => {
assert.equal(inlineWaste.wastedBytes, 6000);
assert.equal(Math.round(inlineWaste.wastedPercent), 29);
});
+
+ it('should augment when provided source maps', async () => {
+ const context = {
+ computedCache: new Map(),
+ options: {
+ // Default threshold is 1024, but is lowered here so that squoosh actually generates
+ // results.
+ // TODO(cjamcl): the bundle visualization feature will require most of the logic currently
+ // done in unused-javascript to be moved to a computed artifact. When that happens, these
+ // tests will go there, and the artifact will not have any thresholds (filtering will happen
+ // within the audits), so this test will not need a threshold. Until then, it does.
+ bundleSourceUnusedThreshold: 100,
+ },
+ };
+ const {map, content, usage} = load('squoosh');
+ const url = 'https://squoosh.app/main-app.js';
+ const networkRecords = [generateRecord(url, content.length, 'Script')];
+ const artifacts = {
+ JsUsage: [usage],
+ devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
+ SourceMaps: [{scriptUrl: url, map}],
+ ScriptElements: [{src: url, content}],
+ };
+ const result = await UnusedJavaScript.audit_(artifacts, networkRecords, context);
+
+ expect(result.items).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "sourceBytes": Array [
+ 10062,
+ 660,
+ 4043,
+ 2138,
+ 4117,
+ ],
+ "sourceWastedBytes": Array [
+ 3760,
+ 660,
+ 500,
+ 293,
+ 256,
+ ],
+ "sources": Array [
+ "(unmapped)",
+ "…src/codecs/webp/encoder-meta.ts",
+ "…src/lib/util.ts",
+ "…src/custom-els/RangeInput/index.ts",
+ "…node_modules/comlink/comlink.js",
+ ],
+ "totalBytes": 83748,
+ "url": "https://squoosh.app/main-app.js",
+ "wastedBytes": 6961,
+ "wastedPercent": 8.312435814764395,
+ },
+ ]
+ `);
+ });
});
});
diff --git a/lighthouse-core/test/computed/js-bundles-test.js b/lighthouse-core/test/computed/js-bundles-test.js
index c4ee55456526..702c58f65575 100644
--- a/lighthouse-core/test/computed/js-bundles-test.js
+++ b/lighthouse-core/test/computed/js-bundles-test.js
@@ -10,9 +10,9 @@ const fs = require('fs');
const JSBundles = require('../../computed/js-bundles.js');
function load(name) {
- const mapData = fs.readFileSync(`${__dirname}/../fixtures/source-maps/${name}.js.map`, 'utf-8');
+ const mapJson = fs.readFileSync(`${__dirname}/../fixtures/source-maps/${name}.js.map`, 'utf-8');
const content = fs.readFileSync(`${__dirname}/../fixtures/source-maps/${name}.js`, 'utf-8');
- return {map: JSON.parse(mapData), content};
+ return {map: JSON.parse(mapJson), content};
}
describe('JSBundles computed artifact', () => {
diff --git a/lighthouse-core/test/config/config-test.js b/lighthouse-core/test/config/config-test.js
index 98f0aec30346..e959064743cc 100644
--- a/lighthouse-core/test/config/config-test.js
+++ b/lighthouse-core/test/config/config-test.js
@@ -217,6 +217,146 @@ describe('Config', () => {
.toEqual(['viewport-dimensions', 'source-maps']);
});
+ // eslint-disable-next-line max-len
+ it('does not throw when an audit requests an optional artifact with no gatherer supplying it', async () => {
+ class DoesntNeedYourCrap extends Audit {
+ static get meta() {
+ return {
+ id: 'optional-artifact-audit',
+ title: 'none',
+ description: 'none',
+ requiredArtifacts: [
+ 'URL', // base artifact
+ 'ViewportDimensions', // from gatherer
+ ],
+ __internalOptionalArtifacts: [
+ 'SourceMaps', // Not in the config.
+ ],
+ };
+ }
+
+ static audit() {}
+ }
+
+ // Shouldn't throw.
+ const config = new Config({
+ extends: 'lighthouse:default',
+ audits: [DoesntNeedYourCrap],
+ }, {
+ // Trigger filtering logic.
+ onlyAudits: ['optional-artifact-audit'],
+ });
+ expect(config.passes[0].gatherers.map(g => g.path)).toEqual(['viewport-dimensions']);
+ });
+
+ it('should keep optional artifacts in gatherers after filter', async () => {
+ class ButWillStillTakeYourCrap extends Audit {
+ static get meta() {
+ return {
+ id: 'optional-artifact-audit',
+ title: 'none',
+ description: 'none',
+ requiredArtifacts: [
+ 'URL', // base artifact
+ 'ViewportDimensions', // from gatherer
+ ],
+ __internalOptionalArtifacts: [
+ 'SourceMaps', // Is in the config.
+ ],
+ };
+ }
+
+ static audit() {}
+ }
+
+ const config = new Config({
+ extends: 'lighthouse:default',
+ // TODO(cjamcl): remove when source-maps is in default config.
+ passes: [{
+ passName: 'defaultPass',
+ gatherers: [
+ 'source-maps',
+ ],
+ }],
+ audits: [ButWillStillTakeYourCrap],
+ }, {
+ // Trigger filtering logic.
+ onlyAudits: ['optional-artifact-audit'],
+ });
+ expect(config.passes[0].gatherers.map(g => g.path))
+ .toEqual(['viewport-dimensions', 'source-maps']);
+ });
+
+ // eslint-disable-next-line max-len
+ it('does not throw when an audit requests an optional artifact with no gatherer supplying it', async () => {
+ class DoesntNeedYourCrap extends Audit {
+ static get meta() {
+ return {
+ id: 'optional-artifact-audit',
+ title: 'none',
+ description: 'none',
+ requiredArtifacts: [
+ 'URL', // base artifact
+ 'ViewportDimensions', // from gatherer
+ ],
+ __internalOptionalArtifacts: [
+ 'SourceMaps', // Not in the config.
+ ],
+ };
+ }
+
+ static audit() {}
+ }
+
+ // Shouldn't throw.
+ const config = new Config({
+ extends: 'lighthouse:default',
+ audits: [DoesntNeedYourCrap],
+ }, {
+ // Trigger filtering logic.
+ onlyAudits: ['optional-artifact-audit'],
+ });
+ expect(config.passes[0].gatherers.map(g => g.path)).toEqual(['viewport-dimensions']);
+ });
+
+ it('should keep optional artifacts in gatherers after filter', async () => {
+ class ButWillStillTakeYourCrap extends Audit {
+ static get meta() {
+ return {
+ id: 'optional-artifact-audit',
+ title: 'none',
+ description: 'none',
+ requiredArtifacts: [
+ 'URL', // base artifact
+ 'ViewportDimensions', // from gatherer
+ ],
+ __internalOptionalArtifacts: [
+ 'SourceMaps', // Is in the config.
+ ],
+ };
+ }
+
+ static audit() {}
+ }
+
+ const config = new Config({
+ extends: 'lighthouse:default',
+ // TODO(cjamcl): remove when source-maps is in default config.
+ passes: [{
+ passName: 'defaultPass',
+ gatherers: [
+ 'source-maps',
+ ],
+ }],
+ audits: [ButWillStillTakeYourCrap],
+ }, {
+ // Trigger filtering logic.
+ onlyAudits: ['optional-artifact-audit'],
+ });
+ expect(config.passes[0].gatherers.map(g => g.path))
+ .toEqual(['viewport-dimensions', 'source-maps']);
+ });
+
it('does not throw when an audit requires only base artifacts', () => {
class BaseArtifactsAudit extends Audit {
static get meta() {
diff --git a/lighthouse-core/test/fixtures/source-maps/squoosh.usage.json b/lighthouse-core/test/fixtures/source-maps/squoosh.usage.json
new file mode 100644
index 000000000000..24e8714d188a
--- /dev/null
+++ b/lighthouse-core/test/fixtures/source-maps/squoosh.usage.json
@@ -0,0 +1,633 @@
+{
+ "url": "https://squoosh.app/main-app.js",
+ "ranges": [
+ {
+ "start": 0,
+ "end": 99
+ },
+ {
+ "start": 347,
+ "end": 1007
+ },
+ {
+ "start": 1042,
+ "end": 1085
+ },
+ {
+ "start": 1093,
+ "end": 1143
+ },
+ {
+ "start": 1149,
+ "end": 1151
+ },
+ {
+ "start": 1246,
+ "end": 1256
+ },
+ {
+ "start": 1276,
+ "end": 1288
+ },
+ {
+ "start": 1308,
+ "end": 1384
+ },
+ {
+ "start": 1404,
+ "end": 1416
+ },
+ {
+ "start": 1436,
+ "end": 1448
+ },
+ {
+ "start": 1468,
+ "end": 1480
+ },
+ {
+ "start": 1500,
+ "end": 1512
+ },
+ {
+ "start": 1532,
+ "end": 1544
+ },
+ {
+ "start": 1564,
+ "end": 1576
+ },
+ {
+ "start": 1596,
+ "end": 1608
+ },
+ {
+ "start": 1628,
+ "end": 1640
+ },
+ {
+ "start": 1660,
+ "end": 1672
+ },
+ {
+ "start": 1692,
+ "end": 1704
+ },
+ {
+ "start": 1724,
+ "end": 1768
+ },
+ {
+ "start": 1788,
+ "end": 1800
+ },
+ {
+ "start": 1820,
+ "end": 2119
+ },
+ {
+ "start": 3346,
+ "end": 3399
+ },
+ {
+ "start": 3532,
+ "end": 3572
+ },
+ {
+ "start": 4148,
+ "end": 4849
+ },
+ {
+ "start": 4869,
+ "end": 4888
+ },
+ {
+ "start": 4908,
+ "end": 4928
+ },
+ {
+ "start": 4948,
+ "end": 4973
+ },
+ {
+ "start": 4993,
+ "end": 5146
+ },
+ {
+ "start": 5166,
+ "end": 5185
+ },
+ {
+ "start": 5205,
+ "end": 5225
+ },
+ {
+ "start": 5245,
+ "end": 5270
+ },
+ {
+ "start": 5290,
+ "end": 5457
+ },
+ {
+ "start": 5477,
+ "end": 5496
+ },
+ {
+ "start": 5516,
+ "end": 5536
+ },
+ {
+ "start": 5556,
+ "end": 5581
+ },
+ {
+ "start": 5601,
+ "end": 5844
+ },
+ {
+ "start": 5864,
+ "end": 5883
+ },
+ {
+ "start": 5903,
+ "end": 5923
+ },
+ {
+ "start": 5943,
+ "end": 5968
+ },
+ {
+ "start": 5988,
+ "end": 6216
+ },
+ {
+ "start": 6236,
+ "end": 6255
+ },
+ {
+ "start": 6275,
+ "end": 6295
+ },
+ {
+ "start": 6315,
+ "end": 6340
+ },
+ {
+ "start": 6360,
+ "end": 6592
+ },
+ {
+ "start": 6612,
+ "end": 6631
+ },
+ {
+ "start": 6651,
+ "end": 6671
+ },
+ {
+ "start": 6691,
+ "end": 6716
+ },
+ {
+ "start": 6736,
+ "end": 6970
+ },
+ {
+ "start": 6990,
+ "end": 7009
+ },
+ {
+ "start": 7029,
+ "end": 7049
+ },
+ {
+ "start": 7069,
+ "end": 7094
+ },
+ {
+ "start": 7114,
+ "end": 7342
+ },
+ {
+ "start": 7362,
+ "end": 7381
+ },
+ {
+ "start": 7401,
+ "end": 7421
+ },
+ {
+ "start": 7441,
+ "end": 7466
+ },
+ {
+ "start": 7486,
+ "end": 10041
+ },
+ {
+ "start": 10061,
+ "end": 10508
+ },
+ {
+ "start": 10580,
+ "end": 10674
+ },
+ {
+ "start": 11094,
+ "end": 11096
+ },
+ {
+ "start": 12402,
+ "end": 13549
+ },
+ {
+ "start": 13569,
+ "end": 13788
+ },
+ {
+ "start": 13798,
+ "end": 13809
+ },
+ {
+ "start": 14664,
+ "end": 14677
+ },
+ {
+ "start": 14684,
+ "end": 14707
+ },
+ {
+ "start": 14717,
+ "end": 14728
+ },
+ {
+ "start": 14813,
+ "end": 14826
+ },
+ {
+ "start": 14861,
+ "end": 14874
+ },
+ {
+ "start": 17654,
+ "end": 17695
+ },
+ {
+ "start": 17715,
+ "end": 17737
+ },
+ {
+ "start": 17784,
+ "end": 17825
+ },
+ {
+ "start": 17845,
+ "end": 17867
+ },
+ {
+ "start": 17914,
+ "end": 17955
+ },
+ {
+ "start": 17975,
+ "end": 17997
+ },
+ {
+ "start": 18058,
+ "end": 18099
+ },
+ {
+ "start": 18119,
+ "end": 18141
+ },
+ {
+ "start": 18202,
+ "end": 18243
+ },
+ {
+ "start": 18263,
+ "end": 18285
+ },
+ {
+ "start": 18332,
+ "end": 18373
+ },
+ {
+ "start": 18393,
+ "end": 18415
+ },
+ {
+ "start": 18462,
+ "end": 18503
+ },
+ {
+ "start": 18523,
+ "end": 18545
+ },
+ {
+ "start": 18592,
+ "end": 18633
+ },
+ {
+ "start": 18653,
+ "end": 18675
+ },
+ {
+ "start": 18722,
+ "end": 18994
+ },
+ {
+ "start": 19090,
+ "end": 19101
+ },
+ {
+ "start": 19507,
+ "end": 19551
+ },
+ {
+ "start": 20082,
+ "end": 20092
+ },
+ {
+ "start": 20112,
+ "end": 20124
+ },
+ {
+ "start": 20144,
+ "end": 20226
+ },
+ {
+ "start": 20247,
+ "end": 20299
+ },
+ {
+ "start": 20320,
+ "end": 20339
+ },
+ {
+ "start": 20360,
+ "end": 20380
+ },
+ {
+ "start": 20401,
+ "end": 20426
+ },
+ {
+ "start": 20447,
+ "end": 20487
+ },
+ {
+ "start": 20508,
+ "end": 20560
+ },
+ {
+ "start": 20581,
+ "end": 20600
+ },
+ {
+ "start": 20621,
+ "end": 20641
+ },
+ {
+ "start": 20662,
+ "end": 20687
+ },
+ {
+ "start": 20708,
+ "end": 20776
+ },
+ {
+ "start": 20797,
+ "end": 20822
+ },
+ {
+ "start": 20843,
+ "end": 20911
+ },
+ {
+ "start": 20932,
+ "end": 20951
+ },
+ {
+ "start": 20972,
+ "end": 20992
+ },
+ {
+ "start": 21013,
+ "end": 21038
+ },
+ {
+ "start": 21059,
+ "end": 21103
+ },
+ {
+ "start": 21441,
+ "end": 21450
+ },
+ {
+ "start": 21497,
+ "end": 21500
+ },
+ {
+ "start": 21506,
+ "end": 21519
+ },
+ {
+ "start": 23765,
+ "end": 23773
+ },
+ {
+ "start": 24059,
+ "end": 24065
+ },
+ {
+ "start": 24236,
+ "end": 24309
+ },
+ {
+ "start": 27423,
+ "end": 27540
+ },
+ {
+ "start": 28176,
+ "end": 28218
+ },
+ {
+ "start": 29523,
+ "end": 29540
+ },
+ {
+ "start": 29869,
+ "end": 29878
+ },
+ {
+ "start": 29981,
+ "end": 29984
+ },
+ {
+ "start": 30154,
+ "end": 30157
+ },
+ {
+ "start": 30423,
+ "end": 30426
+ },
+ {
+ "start": 30739,
+ "end": 30742
+ },
+ {
+ "start": 31029,
+ "end": 31032
+ },
+ {
+ "start": 31131,
+ "end": 31134
+ },
+ {
+ "start": 31215,
+ "end": 31218
+ },
+ {
+ "start": 31418,
+ "end": 31421
+ },
+ {
+ "start": 31644,
+ "end": 31647
+ },
+ {
+ "start": 31750,
+ "end": 31753
+ },
+ {
+ "start": 31871,
+ "end": 31910
+ },
+ {
+ "start": 32400,
+ "end": 32401
+ },
+ {
+ "start": 32732,
+ "end": 33163
+ },
+ {
+ "start": 38213,
+ "end": 39138
+ },
+ {
+ "start": 39587,
+ "end": 39629
+ },
+ {
+ "start": 40887,
+ "end": 41027
+ },
+ {
+ "start": 41056,
+ "end": 41057
+ },
+ {
+ "start": 41123,
+ "end": 41176
+ },
+ {
+ "start": 41505,
+ "end": 41527
+ },
+ {
+ "start": 42063,
+ "end": 42441
+ },
+ {
+ "start": 42777,
+ "end": 43598
+ },
+ {
+ "start": 43823,
+ "end": 43858
+ },
+ {
+ "start": 44757,
+ "end": 44774
+ },
+ {
+ "start": 45103,
+ "end": 45125
+ },
+ {
+ "start": 45394,
+ "end": 45402
+ },
+ {
+ "start": 45806,
+ "end": 45828
+ },
+ {
+ "start": 50509,
+ "end": 51268
+ },
+ {
+ "start": 51573,
+ "end": 52402
+ },
+ {
+ "start": 52481,
+ "end": 52502
+ },
+ {
+ "start": 57643,
+ "end": 58402
+ },
+ {
+ "start": 59428,
+ "end": 59527
+ },
+ {
+ "start": 59571,
+ "end": 59955
+ },
+ {
+ "start": 63962,
+ "end": 65178
+ },
+ {
+ "start": 67149,
+ "end": 67385
+ },
+ {
+ "start": 67975,
+ "end": 68229
+ },
+ {
+ "start": 68408,
+ "end": 68455
+ },
+ {
+ "start": 68830,
+ "end": 68935
+ },
+ {
+ "start": 71888,
+ "end": 71934
+ },
+ {
+ "start": 72394,
+ "end": 72864
+ },
+ {
+ "start": 74270,
+ "end": 74760
+ },
+ {
+ "start": 75092,
+ "end": 75195
+ },
+ {
+ "start": 83549,
+ "end": 83742
+ }
+ ]
+}
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index d4743b801efb..60cf5071b40a 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -2118,5 +2118,6 @@
"systemId": "",
"publicId": ""
},
- "MainDocumentContent": "\n\n\n\n\n\nDoBetterWeb Mega Tester... Of Death\n\n\n\n\n \n\n\n\n\n\n\n\n\n \n \n \n \n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Do better web tester page
\n Hi there!\n\n \n \n\n\n\n\n\n \n external link\n \n external link\n \n external link\n \n external link that uses rel noopener and another unrelated rel attribute\n \n external link that uses rel noreferrer and another unrelated rel attribute\n \n external link that uses rel noopener\n \n external link that uses rel noreferrer\n \n external link that uses rel noopener and noreferrer\n \n internal link is ok\n \n \n \n \n\n\n\n \n \n \n \n \n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
+ "MainDocumentContent": "\n\n\n\n\n\nDoBetterWeb Mega Tester... Of Death\n\n\n\n\n \n\n\n\n\n\n\n\n\n \n \n \n \n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Do better web tester page
\n Hi there!\n\n \n \n\n\n\n\n\n \n external link\n \n external link\n \n external link\n \n external link that uses rel noopener and another unrelated rel attribute\n \n external link that uses rel noreferrer and another unrelated rel attribute\n \n external link that uses rel noopener\n \n external link that uses rel noreferrer\n \n external link that uses rel noopener and noreferrer\n \n internal link is ok\n \n \n \n \n\n\n\n \n \n \n \n \n \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",
+ "SourceMaps": []
}
\ No newline at end of file
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 12cafeff60aa..26ec2d33f047 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -5280,6 +5280,12 @@
"duration": 100,
"entryType": "measure"
},
+ {
+ "startTime": 0,
+ "name": "lh:computed:JSBundles",
+ "duration": 100,
+ "entryType": "measure"
+ },
{
"startTime": 0,
"name": "lh:audit:uses-webp-images",
diff --git a/package.json b/package.json
index 608fc9b05e25..76b353bfb766 100644
--- a/package.json
+++ b/package.json
@@ -195,7 +195,7 @@
},
{
"path": "./dist/lighthouse-dt-bundle.js",
- "maxSize": "440 kB"
+ "maxSize": "450 kB"
},
{
"path": "./dist/lightrider/report-generator-bundle.js",
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index edac8142b0ee..add66f1de4ca 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -5195,6 +5195,12 @@
"name": "lh:audit:unused-javascript",
"startTime": 0.0
},
+ {
+ "duration": 100.0,
+ "entryType": "measure",
+ "name": "lh:computed:JSBundles",
+ "startTime": 0.0
+ },
{
"duration": 100.0,
"entryType": "measure",