From b1420fb07d287fbacfcd93e30d8c6f246dad0b8c Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Mon, 21 May 2018 18:00:38 -0700 Subject: [PATCH 01/11] core(lhr): migrate opportunity details to new format --- lighthouse-core/audits/audit.js | 17 +++ .../byte-efficiency/byte-efficiency-audit.js | 14 +-- .../efficient-animated-content.js | 23 +--- .../byte-efficiency/offscreen-images.js | 21 ++-- .../render-blocking-resources.js | 16 +-- .../audits/byte-efficiency/unminified-css.js | 19 ++- .../byte-efficiency/unminified-javascript.js | 15 ++- .../byte-efficiency/unused-css-rules.js | 18 +-- .../byte-efficiency/unused-javascript.js | 21 ++-- .../byte-efficiency/uses-optimized-images.js | 16 +-- .../byte-efficiency/uses-responsive-images.js | 18 +-- .../byte-efficiency/uses-text-compression.js | 20 +-- .../byte-efficiency/uses-webp-images.js | 16 +-- lighthouse-core/audits/time-to-first-byte.js | 14 ++- lighthouse-core/audits/uses-rel-preconnect.js | 8 +- lighthouse-core/audits/uses-rel-preload.js | 8 +- .../report/html/renderer/category-renderer.js | 4 +- .../report/html/renderer/details-renderer.js | 115 ++++++++++++++---- .../renderer/performance-category-renderer.js | 29 ++--- lighthouse-core/report/html/report-styles.css | 1 + tsconfig.json | 2 +- typings/audit.d.ts | 9 +- typings/lhr-lite.d.ts | 5 +- typings/lhr.d.ts | 17 +++ 24 files changed, 263 insertions(+), 183 deletions(-) diff --git a/lighthouse-core/audits/audit.js b/lighthouse-core/audits/audit.js index a7331faddee9..877cb36eb09e 100644 --- a/lighthouse-core/audits/audit.js +++ b/lighthouse-core/audits/audit.js @@ -125,6 +125,23 @@ class Audit { }; } + /** + * @param {Array} headings + * @param {Array|Array} items + * @param {number} overallSavingsMs + * @param {number=} overallSavingsBytes + * @return {LH.Result.Audit.OpportunityDetails} + */ + static makeOpportunityDetails(headings, items, overallSavingsMs, overallSavingsBytes) { + return { + type: 'opportunity', + headings, + items, + overallSavingsMs, + overallSavingsBytes, + }; + } + /** * @param {typeof Audit} audit * @param {LH.Audit.Product} result diff --git a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js index cfde090a817d..d7f459f0f50e 100644 --- a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +++ b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js @@ -104,7 +104,7 @@ class UnusedBytes extends Audit { * - end time of the last long task in the provided graph * - (if includeLoad is true or not provided) end time of the last node in the graph * - * @param {Array} results The array of byte savings results per resource + * @param {Array} results The array of byte savings results per resource * @param {Node} graph * @param {Simulator} simulator * @param {{includeLoad?: boolean}=} options @@ -114,7 +114,7 @@ class UnusedBytes extends Audit { options = Object.assign({includeLoad: true}, options); const simulationBeforeChanges = simulator.simulate(graph); - /** @type {Map} */ + /** @type {Map} */ const resultsByUrl = new Map(); for (const result of results) { resultsByUrl.set(result.url, result); @@ -165,7 +165,7 @@ class UnusedBytes extends Audit { * @return {LH.Audit.Product} */ static createAuditProduct(result, graph, simulator) { - const results = result.results.sort((itemA, itemB) => itemB.wastedBytes - itemA.wastedBytes); + const results = result.items.sort((itemA, itemB) => itemB.wastedBytes - itemA.wastedBytes); const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0); const wastedKb = Math.round(wastedBytes / KB_IN_BYTES); @@ -177,13 +177,7 @@ class UnusedBytes extends Audit { displayValue = ['Potential savings of %d\xa0KB', wastedKb]; } - const summary = { - wastedMs, - wastedBytes, - }; - - // @ts-ignore - TODO(bckenny): unify details types. items shouldn't be an indexed type. - const details = Audit.makeTableDetails(result.headings, results, summary); + const details = Audit.makeOpportunityDetails(result.headings, results, wastedMs, wastedBytes); return { explanation: result.explanation, diff --git a/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js b/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js index 18e9d736fe3e..9347a5a2dfa2 100644 --- a/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js +++ b/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js @@ -54,7 +54,7 @@ class EfficientAnimatedContent extends ByteEfficiencyAudit { ); /** @type {Array<{url: string, totalBytes: number, wastedBytes: number}>}*/ - const results = unoptimizedContent.map(record => { + const items = unoptimizedContent.map(record => { const resourceSize = record._resourceSize || 0; return { url: record.url, @@ -64,26 +64,15 @@ class EfficientAnimatedContent extends ByteEfficiencyAudit { }; }); + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'url', text: 'URL'}, - { - key: 'totalBytes', - itemType: 'bytes', - displayUnit: 'kb', - granularity: 1, - text: 'Transfer Size', - }, - { - key: 'wastedBytes', - itemType: 'bytes', - displayUnit: 'kb', - granularity: 1, - text: 'Byte Savings', - }, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Transfer Size'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Byte Savings'}, ]; return { - results, + items, headings, }; } diff --git a/lighthouse-core/audits/byte-efficiency/offscreen-images.js b/lighthouse-core/audits/byte-efficiency/offscreen-images.js index f433d072df11..aa80987dd745 100644 --- a/lighthouse-core/audits/byte-efficiency/offscreen-images.js +++ b/lighthouse-core/audits/byte-efficiency/offscreen-images.js @@ -91,7 +91,7 @@ class OffscreenImages extends ByteEfficiencyAudit { * images won't reduce the overall time and the wasted bytes are really only "wasted" for TTI, * override the function to just look at TTI savings. * - * @param {Array} results + * @param {Array} results * @param {LH.Gatherer.Simulation.GraphNode} graph * @param {LH.Gatherer.Simulation.Simulator} simulator * @return {number} @@ -144,7 +144,7 @@ class OffscreenImages extends ByteEfficiencyAudit { // graph simulation doing the right thing. const ttiTimestamp = firstInteractive.timestamp ? firstInteractive.timestamp / 1e6 : Infinity; - const results = Array.from(resultsMap.values()).filter(item => { + const items = Array.from(resultsMap.values()).filter(item => { const isWasteful = item.wastedBytes > IGNORE_THRESHOLD_IN_BYTES && item.wastedPercent > IGNORE_THRESHOLD_IN_PERCENT; @@ -152,22 +152,17 @@ class OffscreenImages extends ByteEfficiencyAudit { return isWasteful && loadedEarly; }); + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'thumbnail', text: ''}, - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - { - key: 'wastedBytes', - itemType: 'bytes', - displayUnit: 'kb', - granularity: 1, - text: 'Potential Savings', - }, + {key: 'url', valueType: 'thumbnail', label: ''}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Potential Savings'}, ]; return { warnings, - results, + items, headings, }; }); diff --git a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js index 1ab379563b18..86fe2ec27c9f 100644 --- a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js +++ b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js @@ -207,20 +207,14 @@ class RenderBlockingResources extends Audit { displayValue = `${results.length} resource delayed first paint by ${wastedMs}ms`; } + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'url', text: 'URL'}, - { - key: 'totalBytes', - itemType: 'bytes', - displayUnit: 'kb', - granularity: 0.01, - text: 'Size (KB)', - }, - {key: 'wastedMs', itemType: 'ms', text: 'Download Time (ms)', granularity: 1}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Size (KB)'}, + {key: 'wastedMs', valueType: 'timespanMs', label: 'Download Time (ms)'}, ]; - const summary = {wastedMs}; - const details = Audit.makeTableDetails(headings, results, summary); + const details = Audit.makeOpportunityDetails(headings, results, wastedMs); return { displayValue, diff --git a/lighthouse-core/audits/byte-efficiency/unminified-css.js b/lighthouse-core/audits/byte-efficiency/unminified-css.js index d061e56100ec..9a5365169a78 100644 --- a/lighthouse-core/audits/byte-efficiency/unminified-css.js +++ b/lighthouse-core/audits/byte-efficiency/unminified-css.js @@ -94,17 +94,17 @@ class UnminifiedCSS extends ByteEfficiencyAudit { * @param {LH.Artifacts.CSSStyleSheetInfo} stylesheet * @param {LH.WebInspector.NetworkRequest=} networkRecord * @param {string} pageUrl - * @return {{url: string|LH.Audit.DetailsRendererCodeDetailJSON, totalBytes: number, wastedBytes: number, wastedPercent: number}} + * @return {{url: string, totalBytes: number, wastedBytes: number, wastedPercent: number}} */ static computeWaste(stylesheet, networkRecord, pageUrl) { const content = stylesheet.content; const totalTokenLength = UnminifiedCSS.computeTokenLength(content); - /** @type {LH.Audit.ByteEfficiencyResult['url']} */ + /** @type {LH.Audit.ByteEfficiencyItem['url']} */ let url = stylesheet.header.sourceURL; if (!url || url === pageUrl) { const contentPreview = UnusedCSSRules.determineContentPreview(stylesheet.content); - url = {type: 'code', value: contentPreview}; + url = contentPreview; } const totalBytes = ByteEfficiencyAudit.estimateTransferSize(networkRecord, content.length, @@ -127,7 +127,7 @@ class UnminifiedCSS extends ByteEfficiencyAudit { */ static audit_(artifacts, networkRecords) { const pageUrl = artifacts.URL.finalUrl; - const results = []; + const items = []; for (const stylesheet of artifacts.CSSUsage.stylesheets) { const networkRecord = networkRecords .find(record => record.url === stylesheet.header.sourceURL); @@ -140,16 +140,15 @@ class UnminifiedCSS extends ByteEfficiencyAudit { if (result.wastedPercent < IGNORE_THRESHOLD_IN_PERCENT || result.wastedBytes < IGNORE_THRESHOLD_IN_BYTES || !Number.isFinite(result.wastedBytes)) continue; - results.push(result); + items.push(result); } return { - results, + items, headings: [ - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - {key: 'wastedBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, - text: 'Potential Savings'}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Potential Savings'}, ], }; } diff --git a/lighthouse-core/audits/byte-efficiency/unminified-javascript.js b/lighthouse-core/audits/byte-efficiency/unminified-javascript.js index 22212d7ec9fb..fbc417c040e4 100644 --- a/lighthouse-core/audits/byte-efficiency/unminified-javascript.js +++ b/lighthouse-core/audits/byte-efficiency/unminified-javascript.js @@ -75,8 +75,8 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit { * @return {LH.Audit.ByteEfficiencyProduct} */ static audit_(artifacts, networkRecords) { - /** @type {Array} */ - const results = []; + /** @type {Array} */ + const items = []; const warnings = []; for (const requestId of Object.keys(artifacts.Scripts)) { const scriptContent = artifacts.Scripts[requestId]; @@ -90,20 +90,19 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit { if (result.wastedPercent < IGNORE_THRESHOLD_IN_PERCENT || result.wastedBytes < IGNORE_THRESHOLD_IN_BYTES || !Number.isFinite(result.wastedBytes)) continue; - results.push(result); + items.push(result); } catch (err) { warnings.push(`Unable to process ${networkRecord._url}: ${err.message}`); } } return { - results, + items, warnings, headings: [ - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - {key: 'wastedBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, - text: 'Potential Savings'}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Potential Savings'}, ], }; } diff --git a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js index 0e2b96c59c42..924dfd809b7b 100644 --- a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js +++ b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js @@ -131,14 +131,14 @@ class UnusedCSSRules extends ByteEfficiencyAudit { /** * @param {StyleSheetInfo} stylesheetInfo The stylesheetInfo object. * @param {string} pageUrl The URL of the page, used to identify inline styles. - * @return {LH.Audit.ByteEfficiencyResult} + * @return {LH.Audit.ByteEfficiencyItem} */ static mapSheetToResult(stylesheetInfo, pageUrl) { - /** @type {LH.Audit.ByteEfficiencyResult['url']} */ + /** @type {LH.Audit.ByteEfficiencyItem['url']} */ let url = stylesheetInfo.header.sourceURL; if (!url || url === pageUrl) { const contentPreview = UnusedCSSRules.determineContentPreview(stylesheetInfo.content); - url = {type: 'code', value: contentPreview}; + url = contentPreview; } const usage = UnusedCSSRules.computeUsage(stylesheetInfo); @@ -159,19 +159,19 @@ class UnusedCSSRules extends ByteEfficiencyAudit { const indexedSheets = UnusedCSSRules.indexStylesheetsById(styles, networkRecords); UnusedCSSRules.indexUsedRules(usage, indexedSheets); - const results = Object.keys(indexedSheets) + const items = Object.keys(indexedSheets) .map(sheetId => UnusedCSSRules.mapSheetToResult(indexedSheets[sheetId], pageUrl)) .filter(sheet => sheet && sheet.wastedBytes > IGNORE_THRESHOLD_IN_BYTES); + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - {key: 'wastedBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, - text: 'Potential Savings'}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Potential Savings'}, ]; return { - results, + items, headings, }; }); diff --git a/lighthouse-core/audits/byte-efficiency/unused-javascript.js b/lighthouse-core/audits/byte-efficiency/unused-javascript.js index a05f98dc1c07..d1200e6eb10c 100644 --- a/lighthouse-core/audits/byte-efficiency/unused-javascript.js +++ b/lighthouse-core/audits/byte-efficiency/unused-javascript.js @@ -62,7 +62,7 @@ class UnusedJavaScript extends ByteEfficiencyAudit { /** * @param {Array<{unusedLength: number, contentLength: number}>} wasteData * @param {LH.WebInspector.NetworkRequest} networkRecord - * @return {LH.Audit.ByteEfficiencyResult} + * @return {LH.Audit.ByteEfficiencyItem} */ static mergeWaste(wasteData, networkRecord) { let unusedLength = 0; @@ -99,24 +99,23 @@ class UnusedJavaScript extends ByteEfficiencyAudit { scriptsByUrl.set(script.url, scripts); } - /** @type {Array} */ - const results = []; + /** @type {Array} */ + const items = []; for (const [url, scripts] of scriptsByUrl.entries()) { const networkRecord = networkRecords.find(record => record.url === url); if (!networkRecord) continue; const wasteData = scripts.map(UnusedJavaScript.computeWaste); - const result = UnusedJavaScript.mergeWaste(wasteData, networkRecord); - if (result.wastedBytes <= IGNORE_THRESHOLD_IN_BYTES) continue; - results.push(result); + const item = UnusedJavaScript.mergeWaste(wasteData, networkRecord); + if (item.wastedBytes <= IGNORE_THRESHOLD_IN_BYTES) continue; + items.push(item); } return { - results, + items, headings: [ - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - {key: 'wastedBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, - text: 'Potential Savings'}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Potential Savings'}, ], }; } diff --git a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js index d8fabec29c8c..0810ab37188f 100644 --- a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js +++ b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js @@ -47,7 +47,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit { const images = artifacts.OptimizedImages; /** @type {Array<{url: string, fromProtocol: boolean, isCrossOrigin: boolean, totalBytes: number, wastedBytes: number}>} */ - const results = []; + const items = []; const warnings = []; for (const image of images) { if (image.failed) { @@ -61,7 +61,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit { const url = URL.elideDataURI(image.url); const jpegSavings = UsesOptimizedImages.computeSavings(image); - results.push({ + items.push({ url, fromProtocol: image.fromProtocol, isCrossOrigin: !image.isSameOrigin, @@ -70,17 +70,17 @@ class UsesOptimizedImages extends ByteEfficiencyAudit { }); } + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'thumbnail', text: ''}, - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - {key: 'wastedBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, - text: 'Potential Savings'}, + {key: 'url', valueType: 'thumbnail', label: ''}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Potential Savings'}, ]; return { warnings, - results, + items, headings, }; } diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js index 8f9419bd0585..722b16955fbf 100644 --- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js +++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js @@ -39,7 +39,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { /** * @param {LH.Artifacts.SingleImageUsage} image * @param {number} DPR devicePixelRatio - * @return {null|Error|LH.Audit.ByteEfficiencyResult}; + * @return {null|Error|LH.Audit.ByteEfficiencyItem}; */ static computeWaste(image, DPR) { // Nothing can be done without network info. @@ -82,7 +82,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { /** @type {string[]} */ const warnings = []; - /** @type {Map} */ + /** @type {Map} */ const resultsMap = new Map(); images.forEach(image => { // TODO: give SVG a free pass until a detail per pixel metric is available @@ -107,20 +107,20 @@ class UsesResponsiveImages extends ByteEfficiencyAudit { } }); - const results = Array.from(resultsMap.values()) + const items = Array.from(resultsMap.values()) .filter(item => item.wastedBytes > IGNORE_THRESHOLD_IN_BYTES); + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'thumbnail', text: ''}, - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - {key: 'wastedBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, - text: 'Potential Savings'}, + {key: 'url', valueType: 'thumbnail', label: ''}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Potential Savings'}, ]; return { warnings, - results, + items, headings, }; } diff --git a/lighthouse-core/audits/byte-efficiency/uses-text-compression.js b/lighthouse-core/audits/byte-efficiency/uses-text-compression.js index 163001359023..936d1e7a41d8 100644 --- a/lighthouse-core/audits/byte-efficiency/uses-text-compression.js +++ b/lighthouse-core/audits/byte-efficiency/uses-text-compression.js @@ -38,8 +38,8 @@ class ResponsesAreCompressed extends ByteEfficiencyAudit { static audit_(artifacts) { const uncompressedResponses = artifacts.ResponseCompression; - /** @type {Array} */ - const results = []; + /** @type {Array} */ + const items = []; uncompressedResponses.forEach(record => { const originalSize = record.resourceSize; const gzipSize = record.gzipSize; @@ -56,28 +56,28 @@ class ResponsesAreCompressed extends ByteEfficiencyAudit { // remove duplicates const url = URL.elideDataURI(record.url); - const isDuplicate = results.find(res => res.url === url && - res.totalBytes === record.resourceSize); + const isDuplicate = items.find(item => item.url === url && + item.totalBytes === record.resourceSize); if (isDuplicate) { return; } - results.push({ + items.push({ url, totalBytes: originalSize, wastedBytes: gzipSavings, }); }); + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'url', text: 'Uncompressed resource URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - {key: 'wastedBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, - text: 'GZIP Savings'}, + {key: 'url', valueType: 'url', label: 'Uncompressed resource URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'GZIP Savings'}, ]; return { - results, + items, headings, }; } diff --git a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js index 8252f6ec08eb..b292e7b831a1 100644 --- a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js +++ b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js @@ -47,7 +47,7 @@ class UsesWebPImages extends ByteEfficiencyAudit { const images = artifacts.OptimizedImages; /** @type {Array<{url: string, fromProtocol: boolean, isCrossOrigin: boolean, totalBytes: number, wastedBytes: number}>} */ - const results = []; + const items = []; const warnings = []; for (const image of images) { if (image.failed) { @@ -60,7 +60,7 @@ class UsesWebPImages extends ByteEfficiencyAudit { const url = URL.elideDataURI(image.url); const webpSavings = UsesWebPImages.computeSavings(image); - results.push({ + items.push({ url, fromProtocol: image.fromProtocol, isCrossOrigin: !image.isSameOrigin, @@ -69,17 +69,17 @@ class UsesWebPImages extends ByteEfficiencyAudit { }); } + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'thumbnail', text: ''}, - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'totalBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, text: 'Original'}, - {key: 'wastedBytes', itemType: 'bytes', displayUnit: 'kb', granularity: 1, - text: 'Potential Savings'}, + {key: 'url', valueType: 'thumbnail', label: ''}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'totalBytes', valueType: 'bytes', label: 'Original'}, + {key: 'wastedBytes', valueType: 'bytes', label: 'Potential Savings'}, ]; return { warnings, - results, + items, headings, }; } diff --git a/lighthouse-core/audits/time-to-first-byte.js b/lighthouse-core/audits/time-to-first-byte.js index 1372cfbf52f1..e331319a30f5 100644 --- a/lighthouse-core/audits/time-to-first-byte.js +++ b/lighthouse-core/audits/time-to-first-byte.js @@ -56,15 +56,19 @@ class TTFBMetric extends Audit { displayValue = `Root document took ${Util.formatMilliseconds(ttfb, 1)} `; } + /** @type {LH.Result.Audit.OpportunityDetails} */ + const details = { + type: 'opportunity', + overallSavingsMs: ttfb - TTFB_THRESHOLD, + headings: [], + items: [], + }; + return { rawValue: ttfb, score: Number(passed), displayValue, - details: { - summary: { - wastedMs: ttfb - TTFB_THRESHOLD, - }, - }, + details, extendedInfo: { value: { wastedMs: ttfb - TTFB_THRESHOLD, diff --git a/lighthouse-core/audits/uses-rel-preconnect.js b/lighthouse-core/audits/uses-rel-preconnect.js index a7b4b2445863..54414b027cb1 100644 --- a/lighthouse-core/audits/uses-rel-preconnect.js +++ b/lighthouse-core/audits/uses-rel-preconnect.js @@ -146,12 +146,12 @@ class UsesRelPreconnectAudit extends Audit { results = results .sort((a, b) => b.wastedMs - a.wastedMs); + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'url', text: 'Origin'}, - {key: 'wastedMs', itemType: 'ms', text: 'Potential Savings'}, + {key: 'url', valueType: 'url', label: 'Origin'}, + {key: 'wastedMs', valueType: 'timespanMs', label: 'Potential Savings'}, ]; - const summary = {wastedMs: maxWasted}; - const details = Audit.makeTableDetails(headings, results, summary); + const details = Audit.makeOpportunityDetails(headings, results, maxWasted); return { score: UnusedBytes.scoreForWastedMs(maxWasted), diff --git a/lighthouse-core/audits/uses-rel-preload.js b/lighthouse-core/audits/uses-rel-preload.js index e6f6c2600bc0..c32410b6efaf 100644 --- a/lighthouse-core/audits/uses-rel-preload.js +++ b/lighthouse-core/audits/uses-rel-preload.js @@ -167,12 +167,12 @@ class UsesRelPreloadAudit extends Audit { // sort results by wastedTime DESC results.sort((a, b) => b.wastedMs - a.wastedMs); + /** @type {LH.Result.Audit.OpportunityDetails['headings']} */ const headings = [ - {key: 'url', itemType: 'url', text: 'URL'}, - {key: 'wastedMs', itemType: 'ms', text: 'Potential Savings', granularity: 10}, + {key: 'url', valueType: 'url', label: 'URL'}, + {key: 'wastedMs', valueType: 'timespanMs', label: 'Potential Savings'}, ]; - const summary = {wastedMs}; - const details = Audit.makeTableDetails(headings, results, summary); + const details = Audit.makeOpportunityDetails(headings, results, wastedMs); return { score: UnusedBytes.scoreForWastedMs(wastedMs), diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js index 1646e6ee6bab..dff8bb77857b 100644 --- a/lighthouse-core/report/html/renderer/category-renderer.js +++ b/lighthouse-core/report/html/renderer/category-renderer.js @@ -105,7 +105,7 @@ class CategoryRenderer { } /** - * @return {!HTMLElement} + * @return {HTMLElement} */ _createChevron() { const chevronTmpl = this.dom.cloneTemplate('#tmpl-lh-chevron', this.templateContext); @@ -114,7 +114,7 @@ class CategoryRenderer { } /** - * @param {!Element} element DOM node to populate with values. + * @param {Element} element DOM node to populate with values. * @param {number|null} score * @param {string} scoreDisplayMode * @return {Element} diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js index b9082090c0d5..9fb463568e59 100644 --- a/lighthouse-core/report/html/renderer/details-renderer.js +++ b/lighthouse-core/report/html/renderer/details-renderer.js @@ -9,6 +9,10 @@ /** @typedef {import('./dom.js')} DOM */ /** @typedef {import('./crc-details-renderer.js')} CRCDetailsJSON */ +/** @typedef {LH.Result.Audit.OpportunityDetails} OpportunityDetails */ + +/** @type {Array} */ +const URL_PREFIXES = ['http://', 'https://', 'data:']; class DetailsRenderer { /** @@ -29,7 +33,7 @@ class DetailsRenderer { } /** - * @param {DetailsJSON} details + * @param {DetailsJSON|OpportunityDetails} details * @return {Element} */ render(details) { @@ -55,13 +59,16 @@ class DetailsRenderer { // @ts-ignore - TODO(bckenny): Fix type hierarchy return this._renderTable(/** @type {TableDetailsJSON} */ (details)); case 'code': - return this._renderCode(details); + return this._renderCode(/** @type {StringDetailsJSON} */ (details)); case 'node': return this.renderNode(/** @type {NodeDetailsJSON} */(details)); case 'criticalrequestchain': return CriticalRequestChainRenderer.render(this._dom, this._templateContext, // @ts-ignore - TODO(bckenny): Fix type hierarchy /** @type {CRCDetailsJSON} */ (details)); + case 'opportunity': + // @ts-ignore - TODO(bckenny): Fix type hierarchy + return this._renderTable(/** @type {OpportunityDetails} */ (details)); default: { throw new Error(`Unknown type: ${details.type}`); } @@ -69,17 +76,17 @@ class DetailsRenderer { } /** - * @param {NumericUnitDetailsJSON} details + * @param {{value: number, granularity?: number}} details * @return {Element} */ _renderBytes(details) { // TODO: handle displayUnit once we have something other than 'kb' const value = Util.formatBytesToKB(details.value, details.granularity); - return this._renderText({type: 'text', value}); + return this._renderText({value}); } /** - * @param {NumericUnitDetailsJSON} details + * @param {{value: number, granularity?: number, displayUnit?: string}} details * @return {Element} */ _renderMilliseconds(details) { @@ -88,11 +95,11 @@ class DetailsRenderer { value = Util.formatDuration(details.value); } - return this._renderText({type: 'text', value}); + return this._renderText({value}); } /** - * @param {StringDetailsJSON} text + * @param {{value: string}} text * @return {HTMLElement} */ _renderTextURL(text) { @@ -116,13 +123,11 @@ class DetailsRenderer { const element = /** @type {HTMLElement} */ (this._dom.createElement('div', 'lh-text__url')); element.appendChild(this._renderText({ value: displayedPath, - type: 'text', })); if (displayedHost) { const hostElem = this._renderText({ value: displayedHost, - type: 'text', }); hostElem.classList.add('lh-text__url-host'); element.appendChild(hostElem); @@ -142,7 +147,6 @@ class DetailsRenderer { if (!allowedProtocols.includes(url.protocol)) { // Fall back to just the link text if protocol not allowed. return this._renderText({ - type: 'text', value: details.text, }); } @@ -157,7 +161,7 @@ class DetailsRenderer { } /** - * @param {StringDetailsJSON} text + * @param {{value: string}} text * @return {Element} */ _renderText(text) { @@ -169,13 +173,11 @@ class DetailsRenderer { /** * Create small thumbnail with scaled down image asset. * If the supplied details doesn't have an image/* mimeType, then an empty span is returned. - * @param {ThumbnailDetails} details + * @param {{value: string}} details * @return {Element} */ _renderThumbnail(details) { const element = /** @type {HTMLImageElement}*/ (this._dom.createElement('img', 'lh-thumbnail')); - /** @type {string} */ - // @ts-ignore - type should have a value if we get here. const strValue = details.value; element.src = strValue; element.title = strValue; @@ -242,6 +244,79 @@ class DetailsRenderer { return tableElem; } + /** + * @param {OpportunityDetails} details + * @return {Element} + */ + _renderOpportunityTable(details) { + if (!details.items.length) return this._dom.createElement('span'); + + const tableElem = this._dom.createElement('table', 'lh-table'); + const theadElem = this._dom.createChildOf(tableElem, 'thead'); + const theadTrElem = this._dom.createChildOf(theadElem, 'tr'); + + for (const heading of details.headings) { + const valueType = heading.valueType || 'text'; + const classes = `lh-table-column--${valueType}`; + const labelEl = this._dom.createElement('div', 'lh-text'); + labelEl.textContent = heading.label; + this._dom.createChildOf(theadTrElem, 'th', classes).appendChild(labelEl); + } + + const tbodyElem = this._dom.createChildOf(tableElem, 'tbody'); + for (const row of details.items) { + const rowElem = this._dom.createChildOf(tbodyElem, 'tr'); + for (const heading of details.headings) { + const key = /** @type {keyof LH.Result.Audit.OpportunityDetailsItem} */ (heading.key); + const value = row[key]; + + if (typeof value === 'undefined' || value === null) { + this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty'); + continue; + } + + const valueType = heading.valueType; + let itemElement; + + switch (valueType) { + case 'url': { + // Fall back to
 rendering if not actually a URL.
+            const strValue = /** @type {string} */ (value);
+            if (URL_PREFIXES.some(prefix => strValue.startsWith(prefix))) {
+              itemElement = this._renderTextURL({value: strValue});
+            } else {
+              const codeValue = /** @type {(number|string|undefined)} */ (value);
+              itemElement = this._renderCode({value: codeValue});
+            }
+            break;
+          }
+          case 'timespanMs': {
+            const numValue = /** @type {number} */ (value);
+            itemElement = this._renderMilliseconds({value: numValue});
+            break;
+          }
+          case 'bytes': {
+            const numValue = /** @type {number} */ (value);
+            itemElement = this._renderBytes({value: numValue});
+            break;
+          }
+          case 'thumbnail': {
+            const strValue = /** @type {string} */ (value);
+            itemElement = this._renderThumbnail({value: strValue});
+            break;
+          }
+          default: {
+            throw new Error(`Unknown valueType: ${valueType}`);
+          }
+        }
+
+        const classes = `lh-table-column--${valueType}`;
+        this._dom.createChildOf(rowElem, 'td', classes).appendChild(itemElement);
+      }
+    }
+    return tableElem;
+  }
+
   /**
    * @param {NodeDetailsJSON} item
    * @return {Element}
@@ -279,7 +354,7 @@ class DetailsRenderer {
   }
 
   /**
-   * @param {DetailsJSON} details
+   * @param {{value?: string|number}} details
    * @return {Element}
    */
   _renderCode(details) {
@@ -300,7 +375,6 @@ if (typeof module !== 'undefined' && module.exports) {
  * @typedef {{
       type: string,
       value: (string|number|undefined),
-      summary?: OpportunitySummary,
       granularity?: number,
       displayUnit?: string
   }} DetailsJSON
@@ -352,7 +426,7 @@ if (typeof module !== 'undefined' && module.exports) {
 
 /** @typedef {{
       type: string,
-      value?: string,
+      value: string,
   }} ThumbnailDetails
  */
 
@@ -369,10 +443,3 @@ if (typeof module !== 'undefined' && module.exports) {
       items: Array<{timing: number, timestamp: number, data: string}>,
   }} FilmstripDetails
  */
-
-
-/** @typedef {{
-      wastedMs?: number,
-      wastedBytes?: number
-  }} OpportunitySummary
- */
diff --git a/lighthouse-core/report/html/renderer/performance-category-renderer.js b/lighthouse-core/report/html/renderer/performance-category-renderer.js
index 71b16678cb02..404f4f0fafd0 100644
--- a/lighthouse-core/report/html/renderer/performance-category-renderer.js
+++ b/lighthouse-core/report/html/renderer/performance-category-renderer.js
@@ -11,8 +11,8 @@
 /** @typedef {import('./report-renderer.js').CategoryJSON} CategoryJSON */
 /** @typedef {import('./report-renderer.js').GroupJSON} GroupJSON */
 /** @typedef {import('./report-renderer.js').AuditJSON} AuditJSON */
-/** @typedef {import('./details-renderer.js').OpportunitySummary} OpportunitySummary */
 /** @typedef {import('./details-renderer.js').FilmstripDetails} FilmstripDetails */
+/** @typedef {LH.Result.Audit.OpportunityDetails} OpportunityDetails */
 
 class PerformanceCategoryRenderer extends CategoryRenderer {
   /**
@@ -56,20 +56,20 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
     const element = this.populateAuditValues(audit, index, oppTmpl);
     element.id = audit.result.id;
 
-    const details = audit.result.details;
-    if (!details) {
+    if (!audit.result.details || audit.result.scoreDisplayMode === 'error') {
       return element;
     }
-    const summaryInfo = /** @type {OpportunitySummary} */ (details.summary);
-    if (!summaryInfo || !summaryInfo.wastedMs || audit.result.scoreDisplayMode === 'error') {
+    // TODO(bckenny): remove cast when details is fully discriminated based on `type`.
+    const details = /** @type {OpportunityDetails} */ (audit.result.details);
+    if (details.type !== 'opportunity') {
       return element;
     }
 
     // Overwrite the displayValue with opportunity's wastedMs
     const displayEl = this.dom.find('.lh-audit__display-text', element);
-    const sparklineWidthPct = `${summaryInfo.wastedMs / scale * 100}%`;
+    const sparklineWidthPct = `${details.overallSavingsMs / scale * 100}%`;
     this.dom.find('.lh-sparkline__bar', element).style.width = sparklineWidthPct;
-    displayEl.textContent = Util.formatSeconds(summaryInfo.wastedMs, 0.01);
+    displayEl.textContent = Util.formatSeconds(details.overallSavingsMs, 0.01);
 
     // Set [title] tooltips
     if (audit.result.displayValue) {
@@ -83,18 +83,19 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
 
   /**
    * Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
-   * Opportunties with an error won't have a summary object, so MIN_VALUE is returned to keep any
+   * Opportunties with an error won't have a details object, so MIN_VALUE is returned to keep any
    * erroring opportunities last in sort order.
    * @param {AuditJSON} audit
    * @return {number}
    */
   _getWastedMs(audit) {
-    if (
-      audit.result.details &&
-      audit.result.details.summary &&
-      typeof audit.result.details.summary.wastedMs === 'number'
-    ) {
-      return audit.result.details.summary.wastedMs;
+    if (audit.result.details && audit.result.details.type === 'opportunity') {
+      // TODO(bckenny): remove cast when details is fully discriminated based on `type`.
+      const details = /** @type {OpportunityDetails} */ (audit.result.details);
+      if (typeof details.overallSavingsMs !== 'number') {
+        throw new Error('non-opportunity details passed to _getWastedMs');
+      }
+      return details.overallSavingsMs;
     } else {
       return Number.MIN_VALUE;
     }
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index c3c9a43cc253..d175f729792f 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -864,6 +864,7 @@ summary.lh-passed-audits-summary {
 
 .lh-table-column--text,
 .lh-table-column--bytes,
+.lh-table-column--timespanMs,
 .lh-table-column--ms {
   text-align: right;
 }
diff --git a/tsconfig.json b/tsconfig.json
index 2fb37692ca43..e21c16499dcd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,7 +12,7 @@
       "./typings"
     ],
 
-    "diagnostics": true
+    "diagnostics": true,
   },
   "include": [
     // TODO(bckenny): unnecessary workaround until https://github.com/Microsoft/TypeScript/issues/24062
diff --git a/typings/audit.d.ts b/typings/audit.d.ts
index c3bb3ffe145d..8a0f1a51e3ac 100644
--- a/typings/audit.d.ts
+++ b/typings/audit.d.ts
@@ -50,16 +50,17 @@ declare global {
       granularity?: number;
     }
 
+    // TODO(bckenny): move these to byte-efficiency-audit.js now that we can import typedefs.
     export interface ByteEfficiencyProduct {
-      results: Array;
-      headings: Array;
+      items: Array;
+      headings: Result.Audit.OpportunityDetails['headings'];
       displayValue?: string;
       explanation?: string;
       warnings?: string[];
     }
 
-    export interface ByteEfficiencyResult {
-      url: string | DetailsRendererCodeDetailJSON;
+    export interface ByteEfficiencyItem extends Result.Audit.OpportunityDetailsItem {
+      url: string;
       wastedBytes: number;
       totalBytes: number;
       wastedPercent?: number;
diff --git a/typings/lhr-lite.d.ts b/typings/lhr-lite.d.ts
index 6b923edc55c0..c5879773ae41 100644
--- a/typings/lhr-lite.d.ts
+++ b/typings/lhr-lite.d.ts
@@ -91,9 +91,12 @@ declare global {
         }
 
         export interface ColumnHeading {
+          /** The property key name within DetailsItem being described. */
           key: string;
+          /** Readable text label of the field. */
           label: string;
-          valueType: 'url' | 'timespanMs' | 'bytes';
+          // TODO(bckenny): should be just string and let lhr be more specific?
+          valueType: 'url' | 'timespanMs' | 'bytes' | 'thumbnail';
         }
 
         export interface WastedBytesDetailsItem {
diff --git a/typings/lhr.d.ts b/typings/lhr.d.ts
index 70d5b17f22b3..2bd4ee59f4bd 100644
--- a/typings/lhr.d.ts
+++ b/typings/lhr.d.ts
@@ -81,6 +81,23 @@ declare global {
         blockedUrlPatterns: string[];
         extraHeaders: Crdp.Network.Headers;
       }
+
+      export module Audit {
+        export interface OpportunityDetailsItem {
+          url: string;
+          wastedBytes?: number;
+          totalBytes?: number;
+          wastedMs?: number;
+          wastedPercent?: number;
+          fromProtocol?: boolean;
+          isCrossOrigin?: boolean;
+          requestStartTime?: number;
+        }
+
+        export interface OpportunityDetails extends ResultLite.Audit.OpportunityDetails {
+          items: OpportunityDetailsItem[];
+        }
+      }
     }
   }
 }

From 0762da4b5464c20b743b637ec7827752ade3fe49 Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Mon, 21 May 2018 18:19:35 -0700
Subject: [PATCH 02/11] update golden LHR for preview of changes

---
 lighthouse-core/test/results/sample_v2.json | 313 +++++++++++++-------
 tsconfig.json                               |   2 +-
 2 files changed, 205 insertions(+), 110 deletions(-)

diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 9ee82156f2d4..0a353e490bb4 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -251,9 +251,10 @@
       "rawValue": 570.5630000000001,
       "displayValue": "",
       "details": {
-        "summary": {
-          "wastedMs": -29.436999999999898
-        }
+        "type": "opportunity",
+        "overallSavingsMs": -29.436999999999898,
+        "headings": [],
+        "items": []
       }
     },
     "first-cpu-idle": {
@@ -786,12 +787,21 @@
         0
       ],
       "details": {
-        "type": "table",
-        "headings": [],
+        "type": "opportunity",
+        "headings": [
+          {
+            "key": "url",
+            "valueType": "url",
+            "label": "URL"
+          },
+          {
+            "key": "wastedMs",
+            "valueType": "timespanMs",
+            "label": "Potential Savings"
+          }
+        ],
         "items": [],
-        "summary": {
-          "wastedMs": 0
-        }
+        "overallSavingsMs": 0
       }
     },
     "uses-rel-preconnect": {
@@ -806,12 +816,21 @@
         0
       ],
       "details": {
-        "type": "table",
-        "headings": [],
+        "type": "opportunity",
+        "headings": [
+          {
+            "key": "url",
+            "valueType": "url",
+            "label": "Origin"
+          },
+          {
+            "key": "wastedMs",
+            "valueType": "timespanMs",
+            "label": "Potential Savings"
+          }
+        ],
         "items": [],
-        "summary": {
-          "wastedMs": 0
-        }
+        "overallSavingsMs": 0
       }
     },
     "font-display": {
@@ -1861,13 +1880,32 @@
       "displayValue": "",
       "warnings": [],
       "details": {
-        "type": "table",
-        "headings": [],
+        "type": "opportunity",
+        "headings": [
+          {
+            "key": "url",
+            "valueType": "thumbnail",
+            "label": ""
+          },
+          {
+            "key": "url",
+            "valueType": "url",
+            "label": "URL"
+          },
+          {
+            "key": "totalBytes",
+            "valueType": "bytes",
+            "label": "Original"
+          },
+          {
+            "key": "wastedBytes",
+            "valueType": "bytes",
+            "label": "Potential Savings"
+          }
+        ],
         "items": [],
-        "summary": {
-          "wastedMs": 0,
-          "wastedBytes": 0
-        }
+        "overallSavingsMs": 0,
+        "overallSavingsBytes": 0
       }
     },
     "render-blocking-resources": {
@@ -1879,25 +1917,22 @@
       "rawValue": 1129,
       "displayValue": "5 resources delayed first paint by 1129ms",
       "details": {
-        "type": "table",
+        "type": "opportunity",
         "headings": [
           {
             "key": "url",
-            "itemType": "url",
-            "text": "URL"
+            "valueType": "url",
+            "label": "URL"
           },
           {
             "key": "totalBytes",
-            "itemType": "bytes",
-            "displayUnit": "kb",
-            "granularity": 0.01,
-            "text": "Size (KB)"
+            "valueType": "bytes",
+            "label": "Size (KB)"
           },
           {
             "key": "wastedMs",
-            "itemType": "ms",
-            "text": "Download Time (ms)",
-            "granularity": 1
+            "valueType": "timespanMs",
+            "label": "Download Time (ms)"
           }
         ],
         "items": [
@@ -1927,9 +1962,7 @@
             "wastedMs": 723
           }
         ],
-        "summary": {
-          "wastedMs": 1129
-        }
+        "overallSavingsMs": 1129
       }
     },
     "unminified-css": {
@@ -1941,13 +1974,27 @@
       "rawValue": 0,
       "displayValue": "",
       "details": {
-        "type": "table",
-        "headings": [],
+        "type": "opportunity",
+        "headings": [
+          {
+            "key": "url",
+            "valueType": "url",
+            "label": "URL"
+          },
+          {
+            "key": "totalBytes",
+            "valueType": "bytes",
+            "label": "Original"
+          },
+          {
+            "key": "wastedBytes",
+            "valueType": "bytes",
+            "label": "Potential Savings"
+          }
+        ],
         "items": [],
-        "summary": {
-          "wastedMs": 0,
-          "wastedBytes": 0
-        }
+        "overallSavingsMs": 0,
+        "overallSavingsBytes": 0
       }
     },
     "unminified-javascript": {
@@ -1963,26 +2010,22 @@
       ],
       "warnings": [],
       "details": {
-        "type": "table",
+        "type": "opportunity",
         "headings": [
           {
             "key": "url",
-            "itemType": "url",
-            "text": "URL"
+            "valueType": "url",
+            "label": "URL"
           },
           {
             "key": "totalBytes",
-            "itemType": "bytes",
-            "displayUnit": "kb",
-            "granularity": 1,
-            "text": "Original"
+            "valueType": "bytes",
+            "label": "Original"
           },
           {
             "key": "wastedBytes",
-            "itemType": "bytes",
-            "displayUnit": "kb",
-            "granularity": 1,
-            "text": "Potential Savings"
+            "valueType": "bytes",
+            "label": "Potential Savings"
           }
         ],
         "items": [
@@ -1993,10 +2036,8 @@
             "wastedPercent": 42.52388078488413
           }
         ],
-        "summary": {
-          "wastedMs": 0,
-          "wastedBytes": 30470
-        }
+        "overallSavingsMs": 0,
+        "overallSavingsBytes": 30470
       }
     },
     "unused-css-rules": {
@@ -2008,13 +2049,27 @@
       "rawValue": 0,
       "displayValue": "",
       "details": {
-        "type": "table",
-        "headings": [],
+        "type": "opportunity",
+        "headings": [
+          {
+            "key": "url",
+            "valueType": "url",
+            "label": "URL"
+          },
+          {
+            "key": "totalBytes",
+            "valueType": "bytes",
+            "label": "Original"
+          },
+          {
+            "key": "wastedBytes",
+            "valueType": "bytes",
+            "label": "Potential Savings"
+          }
+        ],
         "items": [],
-        "summary": {
-          "wastedMs": 0,
-          "wastedBytes": 0
-        }
+        "overallSavingsMs": 0,
+        "overallSavingsBytes": 0
       }
     },
     "uses-webp-images": {
@@ -2030,31 +2085,27 @@
       ],
       "warnings": [],
       "details": {
-        "type": "table",
+        "type": "opportunity",
         "headings": [
           {
             "key": "url",
-            "itemType": "thumbnail",
-            "text": ""
+            "valueType": "thumbnail",
+            "label": ""
           },
           {
             "key": "url",
-            "itemType": "url",
-            "text": "URL"
+            "valueType": "url",
+            "label": "URL"
           },
           {
             "key": "totalBytes",
-            "itemType": "bytes",
-            "displayUnit": "kb",
-            "granularity": 1,
-            "text": "Original"
+            "valueType": "bytes",
+            "label": "Original"
           },
           {
             "key": "wastedBytes",
-            "itemType": "bytes",
-            "displayUnit": "kb",
-            "granularity": 1,
-            "text": "Potential Savings"
+            "valueType": "bytes",
+            "label": "Potential Savings"
           }
         ],
         "items": [
@@ -2066,10 +2117,8 @@
             "wastedBytes": 8526
           }
         ],
-        "summary": {
-          "wastedMs": 0,
-          "wastedBytes": 8526
-        }
+        "overallSavingsMs": 0,
+        "overallSavingsBytes": 8526
       }
     },
     "uses-optimized-images": {
@@ -2082,13 +2131,32 @@
       "displayValue": "",
       "warnings": [],
       "details": {
-        "type": "table",
-        "headings": [],
+        "type": "opportunity",
+        "headings": [
+          {
+            "key": "url",
+            "valueType": "thumbnail",
+            "label": ""
+          },
+          {
+            "key": "url",
+            "valueType": "url",
+            "label": "URL"
+          },
+          {
+            "key": "totalBytes",
+            "valueType": "bytes",
+            "label": "Original"
+          },
+          {
+            "key": "wastedBytes",
+            "valueType": "bytes",
+            "label": "Potential Savings"
+          }
+        ],
         "items": [],
-        "summary": {
-          "wastedMs": 0,
-          "wastedBytes": 0
-        }
+        "overallSavingsMs": 0,
+        "overallSavingsBytes": 0
       }
     },
     "uses-text-compression": {
@@ -2103,26 +2171,22 @@
         63
       ],
       "details": {
-        "type": "table",
+        "type": "opportunity",
         "headings": [
           {
             "key": "url",
-            "itemType": "url",
-            "text": "Uncompressed resource URL"
+            "valueType": "url",
+            "label": "Uncompressed resource URL"
           },
           {
             "key": "totalBytes",
-            "itemType": "bytes",
-            "displayUnit": "kb",
-            "granularity": 1,
-            "text": "Original"
+            "valueType": "bytes",
+            "label": "Original"
           },
           {
             "key": "wastedBytes",
-            "itemType": "bytes",
-            "displayUnit": "kb",
-            "granularity": 1,
-            "text": "GZIP Savings"
+            "valueType": "bytes",
+            "label": "GZIP Savings"
           }
         ],
         "items": [
@@ -2137,10 +2201,8 @@
             "wastedBytes": 8442
           }
         ],
-        "summary": {
-          "wastedMs": 150,
-          "wastedBytes": 64646
-        }
+        "overallSavingsMs": 150,
+        "overallSavingsBytes": 64646
       }
     },
     "uses-responsive-images": {
@@ -2153,13 +2215,32 @@
       "displayValue": "",
       "warnings": [],
       "details": {
-        "type": "table",
-        "headings": [],
+        "type": "opportunity",
+        "headings": [
+          {
+            "key": "url",
+            "valueType": "thumbnail",
+            "label": ""
+          },
+          {
+            "key": "url",
+            "valueType": "url",
+            "label": "URL"
+          },
+          {
+            "key": "totalBytes",
+            "valueType": "bytes",
+            "label": "Original"
+          },
+          {
+            "key": "wastedBytes",
+            "valueType": "bytes",
+            "label": "Potential Savings"
+          }
+        ],
         "items": [],
-        "summary": {
-          "wastedMs": 0,
-          "wastedBytes": 0
-        }
+        "overallSavingsMs": 0,
+        "overallSavingsBytes": 0
       }
     },
     "efficient-animated-content": {
@@ -2171,13 +2252,27 @@
       "rawValue": 0,
       "displayValue": "",
       "details": {
-        "type": "table",
-        "headings": [],
+        "type": "opportunity",
+        "headings": [
+          {
+            "key": "url",
+            "valueType": "url",
+            "label": "URL"
+          },
+          {
+            "key": "totalBytes",
+            "valueType": "bytes",
+            "label": "Transfer Size"
+          },
+          {
+            "key": "wastedBytes",
+            "valueType": "bytes",
+            "label": "Byte Savings"
+          }
+        ],
         "items": [],
-        "summary": {
-          "wastedMs": 0,
-          "wastedBytes": 0
-        }
+        "overallSavingsMs": 0,
+        "overallSavingsBytes": 0
       }
     },
     "appcache-manifest": {
diff --git a/tsconfig.json b/tsconfig.json
index e21c16499dcd..2fb37692ca43 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,7 +12,7 @@
       "./typings"
     ],
 
-    "diagnostics": true,
+    "diagnostics": true
   },
   "include": [
     // TODO(bckenny): unnecessary workaround until https://github.com/Microsoft/TypeScript/issues/24062

From 4b0c45649516b5d5290b00b09d1b311c3f500846 Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Tue, 22 May 2018 12:07:00 -0700
Subject: [PATCH 03/11] feedback

---
 lighthouse-core/audits/byte-efficiency/unused-css-rules.js | 4 ++--
 lighthouse-core/report/html/renderer/details-renderer.js   | 2 +-
 typings/lhr-lite.d.ts                                      | 2 ++
 typings/lhr.d.ts                                           | 5 +----
 4 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
index 924dfd809b7b..e8516be27ac4 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
@@ -134,7 +134,6 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
    * @return {LH.Audit.ByteEfficiencyItem}
    */
   static mapSheetToResult(stylesheetInfo, pageUrl) {
-    /** @type {LH.Audit.ByteEfficiencyItem['url']} */
     let url = stylesheetInfo.header.sourceURL;
     if (!url || url === pageUrl) {
       const contentPreview = UnusedCSSRules.determineContentPreview(stylesheetInfo.content);
@@ -142,7 +141,8 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
     }
 
     const usage = UnusedCSSRules.computeUsage(stylesheetInfo);
-    return Object.assign({url}, usage);
+    const result = {url}; // Assign to temporary to keep tsc happy about index signature.
+    return Object.assign(result, usage);
   }
 
   /**
diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index 9fb463568e59..09bce9690ab3 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -57,7 +57,7 @@ class DetailsRenderer {
         return this._renderFilmstrip(/** @type {FilmstripDetails} */ (details));
       case 'table':
         // @ts-ignore - TODO(bckenny): Fix type hierarchy
-        return this._renderTable(/** @type {TableDetailsJSON} */ (details));
+        return this._renderOpportunityTable(details);
       case 'code':
         return this._renderCode(/** @type {StringDetailsJSON} */ (details));
       case 'node':
diff --git a/typings/lhr-lite.d.ts b/typings/lhr-lite.d.ts
index c5879773ae41..dc7def56f41c 100644
--- a/typings/lhr-lite.d.ts
+++ b/typings/lhr-lite.d.ts
@@ -103,12 +103,14 @@ declare global {
           url: string;
           wastedBytes?: number;
           totalBytes?: number;
+          [p: string]: number | boolean | string | undefined;
         }
 
         export interface WastedTimeDetailsItem {
           url: string;
           wastedMs: number;
           totalBytes?: number;
+          [p: string]: number | boolean | string | undefined;
         }
       }
     }
diff --git a/typings/lhr.d.ts b/typings/lhr.d.ts
index 2bd4ee59f4bd..32639bb05b63 100644
--- a/typings/lhr.d.ts
+++ b/typings/lhr.d.ts
@@ -88,10 +88,7 @@ declare global {
           wastedBytes?: number;
           totalBytes?: number;
           wastedMs?: number;
-          wastedPercent?: number;
-          fromProtocol?: boolean;
-          isCrossOrigin?: boolean;
-          requestStartTime?: number;
+          [p: string]: number | boolean | string | undefined;
         }
 
         export interface OpportunityDetails extends ResultLite.Audit.OpportunityDetails {

From 9f4d57a613abed041eaa3b3c7a979c87f767b9e6 Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Tue, 22 May 2018 12:16:51 -0700
Subject: [PATCH 04/11] eliminate excess type decls

---
 lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js | 2 +-
 lighthouse-core/audits/byte-efficiency/unminified-css.js        | 1 -
 lighthouse-core/audits/byte-efficiency/unused-javascript.js     | 1 -
 .../audits/byte-efficiency/uses-responsive-images.js            | 2 +-
 lighthouse-core/audits/byte-efficiency/uses-webp-images.js      | 2 +-
 5 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
index d7f459f0f50e..80f4b4f7497d 100644
--- a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
+++ b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
@@ -114,7 +114,7 @@ class UnusedBytes extends Audit {
     options = Object.assign({includeLoad: true}, options);
 
     const simulationBeforeChanges = simulator.simulate(graph);
-    /** @type {Map} */
+    /** @type {Map} */
     const resultsByUrl = new Map();
     for (const result of results) {
       resultsByUrl.set(result.url, result);
diff --git a/lighthouse-core/audits/byte-efficiency/unminified-css.js b/lighthouse-core/audits/byte-efficiency/unminified-css.js
index 9a5365169a78..3ce3ce563a15 100644
--- a/lighthouse-core/audits/byte-efficiency/unminified-css.js
+++ b/lighthouse-core/audits/byte-efficiency/unminified-css.js
@@ -100,7 +100,6 @@ class UnminifiedCSS extends ByteEfficiencyAudit {
     const content = stylesheet.content;
     const totalTokenLength = UnminifiedCSS.computeTokenLength(content);
 
-    /** @type {LH.Audit.ByteEfficiencyItem['url']} */
     let url = stylesheet.header.sourceURL;
     if (!url || url === pageUrl) {
       const contentPreview = UnusedCSSRules.determineContentPreview(stylesheet.content);
diff --git a/lighthouse-core/audits/byte-efficiency/unused-javascript.js b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
index d1200e6eb10c..abfd8c5caa51 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-javascript.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
@@ -99,7 +99,6 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
       scriptsByUrl.set(script.url, scripts);
     }
 
-    /** @type {Array} */
     const items = [];
     for (const [url, scripts] of scriptsByUrl.entries()) {
       const networkRecord = networkRecords.find(record => record.url === url);
diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
index 722b16955fbf..e7412b536f5b 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
@@ -82,7 +82,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
 
     /** @type {string[]} */
     const warnings = [];
-    /** @type {Map} */
+    /** @type {Map} */
     const resultsMap = new Map();
     images.forEach(image => {
       // TODO: give SVG a free pass until a detail per pixel metric is available
diff --git a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
index b292e7b831a1..9b8c94c2c13d 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
@@ -46,7 +46,7 @@ class UsesWebPImages extends ByteEfficiencyAudit {
   static audit_(artifacts) {
     const images = artifacts.OptimizedImages;
 
-    /** @type {Array<{url: string, fromProtocol: boolean, isCrossOrigin: boolean, totalBytes: number, wastedBytes: number}>} */
+    /** @type {Array} */
     const items = [];
     const warnings = [];
     for (const image of images) {

From 78b5aef7a91aa335f37fd2beceb9887028d37c40 Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Tue, 22 May 2018 12:24:28 -0700
Subject: [PATCH 05/11] whoops

---
 lighthouse-core/report/html/renderer/details-renderer.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index 09bce9690ab3..33d73a7ac0c8 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -57,7 +57,7 @@ class DetailsRenderer {
         return this._renderFilmstrip(/** @type {FilmstripDetails} */ (details));
       case 'table':
         // @ts-ignore - TODO(bckenny): Fix type hierarchy
-        return this._renderOpportunityTable(details);
+        return this._renderTable(/** @type {TableDetailsJSON} */ (details));
       case 'code':
         return this._renderCode(/** @type {StringDetailsJSON} */ (details));
       case 'node':
@@ -68,7 +68,7 @@ class DetailsRenderer {
           /** @type {CRCDetailsJSON} */ (details));
       case 'opportunity':
         // @ts-ignore - TODO(bckenny): Fix type hierarchy
-        return this._renderTable(/** @type {OpportunityDetails} */ (details));
+        return this._renderOpportunityTable(details);
       default: {
         throw new Error(`Unknown type: ${details.type}`);
       }

From 4b2ce68300406364e4e55b4bffb9028a5cce0365 Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Tue, 22 May 2018 12:45:20 -0700
Subject: [PATCH 06/11] feedback

---
 .../audits/byte-efficiency/byte-efficiency-audit.js | 13 +++++++++++--
 .../byte-efficiency/efficient-animated-content.js   |  2 +-
 .../audits/byte-efficiency/offscreen-images.js      |  2 +-
 .../audits/byte-efficiency/unminified-css.js        |  2 +-
 .../audits/byte-efficiency/unminified-javascript.js |  2 +-
 .../audits/byte-efficiency/unused-css-rules.js      |  2 +-
 .../audits/byte-efficiency/unused-javascript.js     |  2 +-
 .../audits/byte-efficiency/uses-optimized-images.js |  2 +-
 .../byte-efficiency/uses-responsive-images.js       |  2 +-
 .../audits/byte-efficiency/uses-text-compression.js |  2 +-
 .../audits/byte-efficiency/uses-webp-images.js      |  2 +-
 .../report/html/renderer/details-renderer.js        |  2 ++
 typings/audit.d.ts                                  |  9 ---------
 13 files changed, 23 insertions(+), 21 deletions(-)

diff --git a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
index 80f4b4f7497d..4782e9ccc58b 100644
--- a/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
+++ b/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js
@@ -17,6 +17,15 @@ const KB_IN_BYTES = 1024;
 const WASTED_MS_FOR_AVERAGE = 300;
 const WASTED_MS_FOR_POOR = 750;
 
+/**
+ * @typedef {object} ByteEfficiencyProduct
+ * @property {Array} items
+ * @property {LH.Result.Audit.OpportunityDetails['headings']} headings
+ * @property {string} [displayValue]
+ * @property {string} [explanation]
+ * @property {Array} [warnings]
+ */
+
 /**
  * @overview Used as the base for all byte efficiency audits. Computes total bytes
  *    and estimated time saved. Subclass and override `audit_` to return results.
@@ -159,7 +168,7 @@ class UnusedBytes extends Audit {
   }
 
   /**
-   * @param {LH.Audit.ByteEfficiencyProduct} result
+   * @param {ByteEfficiencyProduct} result
    * @param {Node} graph
    * @param {Simulator} simulator
    * @return {LH.Audit.Product}
@@ -202,7 +211,7 @@ class UnusedBytes extends Audit {
    * @param {LH.Artifacts} artifacts
    * @param {Array} networkRecords
    * @param {LH.Audit.Context} context
-   * @return {LH.Audit.ByteEfficiencyProduct|Promise}
+   * @return {ByteEfficiencyProduct|Promise}
    */
   static audit_(artifacts, networkRecords, context) {
     throw new Error('audit_ unimplemented');
diff --git a/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js b/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js
index 9347a5a2dfa2..94de68db5d6b 100644
--- a/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js
+++ b/lighthouse-core/audits/byte-efficiency/efficient-animated-content.js
@@ -44,7 +44,7 @@ class EfficientAnimatedContent extends ByteEfficiencyAudit {
   /**
    * @param {LH.Artifacts} artifacts
    * @param {Array} networkRecords
-   * @return {LH.Audit.ByteEfficiencyProduct}
+   * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts, networkRecords) {
     const unoptimizedContent = networkRecords.filter(
diff --git a/lighthouse-core/audits/byte-efficiency/offscreen-images.js b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
index aa80987dd745..ce350ff0817d 100644
--- a/lighthouse-core/audits/byte-efficiency/offscreen-images.js
+++ b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
@@ -105,7 +105,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
    * @param {LH.Artifacts} artifacts
    * @param {Array} networkRecords
    * @param {LH.Audit.Context} context
-   * @return {Promise}
+   * @return {Promise}
    */
   static audit_(artifacts, networkRecords, context) {
     const images = artifacts.ImageUsage;
diff --git a/lighthouse-core/audits/byte-efficiency/unminified-css.js b/lighthouse-core/audits/byte-efficiency/unminified-css.js
index 3ce3ce563a15..b9e34bd07749 100644
--- a/lighthouse-core/audits/byte-efficiency/unminified-css.js
+++ b/lighthouse-core/audits/byte-efficiency/unminified-css.js
@@ -122,7 +122,7 @@ class UnminifiedCSS extends ByteEfficiencyAudit {
   /**
    * @param {LH.Artifacts} artifacts
    * @param {Array} networkRecords
-   * @return {LH.Audit.ByteEfficiencyProduct}
+   * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts, networkRecords) {
     const pageUrl = artifacts.URL.finalUrl;
diff --git a/lighthouse-core/audits/byte-efficiency/unminified-javascript.js b/lighthouse-core/audits/byte-efficiency/unminified-javascript.js
index fbc417c040e4..f93afec34a2d 100644
--- a/lighthouse-core/audits/byte-efficiency/unminified-javascript.js
+++ b/lighthouse-core/audits/byte-efficiency/unminified-javascript.js
@@ -72,7 +72,7 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit {
   /**
    * @param {LH.Artifacts} artifacts
    * @param {Array} networkRecords
-   * @return {LH.Audit.ByteEfficiencyProduct}
+   * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts, networkRecords) {
     /** @type {Array} */
diff --git a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
index e8516be27ac4..2748b855d4f1 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
@@ -147,7 +147,7 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
 
   /**
    * @param {LH.Artifacts} artifacts
-   * @return {Promise}
+   * @return {Promise}
    */
   static audit_(artifacts) {
     const styles = artifacts.CSSUsage.stylesheets;
diff --git a/lighthouse-core/audits/byte-efficiency/unused-javascript.js b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
index abfd8c5caa51..04273a547db0 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-javascript.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
@@ -88,7 +88,7 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
   /**
    * @param {LH.Artifacts} artifacts
    * @param {Array} networkRecords
-   * @return {LH.Audit.ByteEfficiencyProduct}
+   * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts, networkRecords) {
     /** @type {Map>} */
diff --git a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
index 0810ab37188f..c4742393b578 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
@@ -41,7 +41,7 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
 
   /**
    * @param {LH.Artifacts} artifacts
-   * @return {LH.Audit.ByteEfficiencyProduct}
+   * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts) {
     const images = artifacts.OptimizedImages;
diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
index e7412b536f5b..88a2932baa5c 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
@@ -74,7 +74,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
 
   /**
    * @param {LH.Artifacts} artifacts
-   * @return {LH.Audit.ByteEfficiencyProduct}
+   * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts) {
     const images = artifacts.ImageUsage;
diff --git a/lighthouse-core/audits/byte-efficiency/uses-text-compression.js b/lighthouse-core/audits/byte-efficiency/uses-text-compression.js
index 936d1e7a41d8..16c000ed1ddd 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-text-compression.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-text-compression.js
@@ -33,7 +33,7 @@ class ResponsesAreCompressed extends ByteEfficiencyAudit {
 
   /**
    * @param {LH.Artifacts} artifacts
-   * @return {LH.Audit.ByteEfficiencyProduct}
+   * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts) {
     const uncompressedResponses = artifacts.ResponseCompression;
diff --git a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
index 9b8c94c2c13d..68f638b6aa8d 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-webp-images.js
@@ -41,7 +41,7 @@ class UsesWebPImages extends ByteEfficiencyAudit {
 
   /**
    * @param {LH.Artifacts} artifacts
-   * @return {LH.Audit.ByteEfficiencyProduct}
+   * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts) {
     const images = artifacts.OptimizedImages;
diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index 33d73a7ac0c8..456fd93cc8e9 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -245,6 +245,8 @@ class DetailsRenderer {
   }
 
   /**
+   * TODO(bckenny): migrate remaining table rendering to this function, then rename
+   * back to _renderTable and replace the original.
    * @param {OpportunityDetails} details
    * @return {Element}
    */
diff --git a/typings/audit.d.ts b/typings/audit.d.ts
index 8a0f1a51e3ac..a0a6c804a562 100644
--- a/typings/audit.d.ts
+++ b/typings/audit.d.ts
@@ -50,15 +50,6 @@ declare global {
       granularity?: number;
     }
 
-    // TODO(bckenny): move these to byte-efficiency-audit.js now that we can import typedefs.
-    export interface ByteEfficiencyProduct {
-      items: Array;
-      headings: Result.Audit.OpportunityDetails['headings'];
-      displayValue?: string;
-      explanation?: string;
-      warnings?: string[];
-    }
-
     export interface ByteEfficiencyItem extends Result.Audit.OpportunityDetailsItem {
       url: string;
       wastedBytes: number;

From 6cac8fcd84a70964a81bf6241a4a5aa9b8d2428c Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Tue, 22 May 2018 14:44:43 -0700
Subject: [PATCH 07/11] fix unit tests

---
 .../byte-efficiency-audit-test.js             | 26 +++++++++----------
 .../efficient-animated-content-test.js        | 18 ++++++-------
 .../byte-efficiency/offscreen-images-test.js  | 12 ++++-----
 .../byte-efficiency/unminified-css-test.js    | 16 ++++++------
 .../unminified-javascript-test.js             | 22 ++++++++--------
 .../byte-efficiency/unused-css-rules-test.js  | 24 ++++++++---------
 .../byte-efficiency/unused-javascript-test.js |  6 ++---
 .../uses-optimized-images-test.js             | 10 +++----
 .../uses-responsive-images-test.js            | 14 +++++-----
 .../uses-text-compression-test.js             |  4 +--
 10 files changed, 76 insertions(+), 76 deletions(-)

diff --git a/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js b/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
index bb595521fd76..d4aa061f16ba 100644
--- a/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/byte-efficiency-audit-test.js
@@ -78,7 +78,7 @@ describe('Byte efficiency base audit', () => {
   it('should format details', () => {
     const result = ByteEfficiencyAudit.createAuditProduct({
       headings: baseHeadings,
-      results: [],
+      items: [],
     }, graph, simulator);
 
     assert.deepEqual(result.details.items, []);
@@ -88,7 +88,7 @@ describe('Byte efficiency base audit', () => {
     const result = ByteEfficiencyAudit.createAuditProduct(
       {
         headings: baseHeadings,
-        results: [
+        items: [
           {url: 'http://example.com/', wastedBytes: 200 * 1000},
         ],
       },
@@ -103,22 +103,22 @@ describe('Byte efficiency base audit', () => {
   it('should score the wastedMs', () => {
     const perfectResult = ByteEfficiencyAudit.createAuditProduct({
       headings: baseHeadings,
-      results: [{url: 'http://example.com/', wastedBytes: 1 * 1000}],
+      items: [{url: 'http://example.com/', wastedBytes: 1 * 1000}],
     }, graph, simulator);
 
     const goodResult = ByteEfficiencyAudit.createAuditProduct({
       headings: baseHeadings,
-      results: [{url: 'http://example.com/', wastedBytes: 20 * 1000}],
+      items: [{url: 'http://example.com/', wastedBytes: 20 * 1000}],
     }, graph, simulator);
 
     const averageResult = ByteEfficiencyAudit.createAuditProduct({
       headings: baseHeadings,
-      results: [{url: 'http://example.com/', wastedBytes: 100 * 1000}],
+      items: [{url: 'http://example.com/', wastedBytes: 100 * 1000}],
     }, graph, simulator);
 
     const failingResult = ByteEfficiencyAudit.createAuditProduct({
       headings: baseHeadings,
-      results: [{url: 'http://example.com/', wastedBytes: 400 * 1000}],
+      items: [{url: 'http://example.com/', wastedBytes: 400 * 1000}],
     }, graph, simulator);
 
     assert.equal(perfectResult.score, 1, 'scores perfect wastedMs');
@@ -131,7 +131,7 @@ describe('Byte efficiency base audit', () => {
     assert.throws(() => {
       ByteEfficiencyAudit.createAuditProduct({
         headings: baseHeadings,
-        results: [{wastedBytes: 350, totalBytes: 700, wastedPercent: 50}],
+        items: [{wastedBytes: 350, totalBytes: 700, wastedPercent: 50}],
       }, null);
     });
   });
@@ -139,7 +139,7 @@ describe('Byte efficiency base audit', () => {
   it('should populate KB', () => {
     const result = ByteEfficiencyAudit.createAuditProduct({
       headings: baseHeadings,
-      results: [
+      items: [
         {wastedBytes: 2048, totalBytes: 4096, wastedPercent: 50},
         {wastedBytes: 1986, totalBytes: 5436},
       ],
@@ -154,7 +154,7 @@ describe('Byte efficiency base audit', () => {
   it('should sort on wastedBytes', () => {
     const result = ByteEfficiencyAudit.createAuditProduct({
       headings: baseHeadings,
-      results: [
+      items: [
         {wastedBytes: 350, totalBytes: 700, wastedPercent: 50},
         {wastedBytes: 450, totalBytes: 1000, wastedPercent: 50},
         {wastedBytes: 400, totalBytes: 450, wastedPercent: 50},
@@ -169,7 +169,7 @@ describe('Byte efficiency base audit', () => {
   it('should create a display value', () => {
     const result = ByteEfficiencyAudit.createAuditProduct({
       headings: baseHeadings,
-      results: [
+      items: [
         {wastedBytes: 512, totalBytes: 700, wastedPercent: 50},
         {wastedBytes: 512, totalBytes: 1000, wastedPercent: 50},
         {wastedBytes: 1024, totalBytes: 1200, wastedPercent: 50},
@@ -188,7 +188,7 @@ describe('Byte efficiency base audit', () => {
     const result = ByteEfficiencyAudit.createAuditProduct(
       {
         headings: [{key: 'value', text: 'Label'}],
-        results: [
+        items: [
           {url: 'https://www.googletagmanager.com/gtm.js?id=GTM-Q5SW', wastedBytes: 30 * 1024},
         ],
       },
@@ -203,7 +203,7 @@ describe('Byte efficiency base audit', () => {
     class MockAudit extends ByteEfficiencyAudit {
       static audit_(artifacts, records) {
         return {
-          results: records.map(record => ({url: record.url, wastedBytes: record.transferSize})),
+          items: records.map(record => ({url: record.url, wastedBytes: record.transferSize})),
           headings: [],
         };
       }
@@ -230,7 +230,7 @@ describe('Byte efficiency base audit', () => {
     class MockAudit extends ByteEfficiencyAudit {
       static audit_(artifacts, records) {
         return {
-          results: records.map(record => ({url: record.url, wastedBytes: record.transferSize})),
+          items: records.map(record => ({url: record.url, wastedBytes: record.transferSize})),
           headings: [],
         };
       }
diff --git a/lighthouse-core/test/audits/byte-efficiency/efficient-animated-content-test.js b/lighthouse-core/test/audits/byte-efficiency/efficient-animated-content-test.js
index fe0bd9a276e3..f2b727c0e6fd 100644
--- a/lighthouse-core/test/audits/byte-efficiency/efficient-animated-content-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/efficient-animated-content-test.js
@@ -31,11 +31,11 @@ describe('Page uses videos for animated GIFs', () => {
       devtoolsLogs: {[EfficientAnimatedContent.DEFAULT_PASS]: []},
     };
 
-    const {results} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
-    assert.equal(results.length, 1);
-    assert.equal(results[0].url, 'https://example.com/example2.gif');
-    assert.equal(results[0].totalBytes, 110000);
-    assert.equal(Math.round(results[0].wastedBytes), 50600);
+    const {items} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
+    assert.equal(items.length, 1);
+    assert.equal(items[0].url, 'https://example.com/example2.gif');
+    assert.equal(items[0].totalBytes, 110000);
+    assert.equal(Math.round(items[0].wastedBytes), 50600);
   });
 
   it(`shouldn't flag content that looks like a gif but isn't`, async () => {
@@ -50,8 +50,8 @@ describe('Page uses videos for animated GIFs', () => {
       devtoolsLogs: {[EfficientAnimatedContent.DEFAULT_PASS]: []},
     };
 
-    const {results} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
-    assert.equal(results.length, 0);
+    const {items} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
+    assert.equal(items.length, 0);
   });
 
   it(`shouldn't flag non gif content`, async () => {
@@ -71,7 +71,7 @@ describe('Page uses videos for animated GIFs', () => {
       devtoolsLogs: {[EfficientAnimatedContent.DEFAULT_PASS]: []},
     };
 
-    const {results} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
-    assert.equal(results.length, 0);
+    const {items} = await EfficientAnimatedContent.audit_(artifacts, networkRecords);
+    assert.equal(items.length, 0);
   });
 });
diff --git a/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js b/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
index 733f7f4cc28f..8f96ff505f65 100644
--- a/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
@@ -61,7 +61,7 @@ describe('OffscreenImages audit', () => {
       devtoolsLogs: {},
       requestFirstCPUIdle: generateInteractiveFunc(2),
     }, [], {}).then(auditResult => {
-      assert.equal(auditResult.results.length, 0);
+      assert.equal(auditResult.items.length, 0);
     });
   });
 
@@ -79,7 +79,7 @@ describe('OffscreenImages audit', () => {
       devtoolsLogs: {},
       requestFirstCPUIdle: generateInteractiveFunc(2),
     }, [], {}).then(auditResult => {
-      assert.equal(auditResult.results.length, 0);
+      assert.equal(auditResult.items.length, 0);
     });
   });
 
@@ -103,7 +103,7 @@ describe('OffscreenImages audit', () => {
       devtoolsLogs: {},
       requestFirstCPUIdle: generateInteractiveFunc(2),
     }, [], {}).then(auditResult => {
-      assert.equal(auditResult.results.length, 4);
+      assert.equal(auditResult.items.length, 4);
     });
   });
 
@@ -117,7 +117,7 @@ describe('OffscreenImages audit', () => {
       devtoolsLogs: {},
       requestFirstCPUIdle: generateInteractiveFunc(2),
     }, [], {}).then(auditResult => {
-      assert.equal(auditResult.results.length, 1);
+      assert.equal(auditResult.items.length, 1);
     });
   });
 
@@ -135,7 +135,7 @@ describe('OffscreenImages audit', () => {
       devtoolsLogs: {},
       requestFirstCPUIdle: generateInteractiveFunc(2),
     }, [], {}).then(auditResult => {
-      assert.equal(auditResult.results.length, 1);
+      assert.equal(auditResult.items.length, 1);
     });
   });
 
@@ -150,7 +150,7 @@ describe('OffscreenImages audit', () => {
       devtoolsLogs: {},
       requestFirstCPUIdle: generateInteractiveFunc(2),
     }, [], {}, [], {}).then(auditResult => {
-      assert.equal(auditResult.results.length, 0);
+      assert.equal(auditResult.items.length, 0);
     });
   });
 });
diff --git a/lighthouse-core/test/audits/byte-efficiency/unminified-css-test.js b/lighthouse-core/test/audits/byte-efficiency/unminified-css-test.js
index 98894537c10f..83c7a867d302 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unminified-css-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unminified-css-test.js
@@ -153,13 +153,13 @@ describe('Page uses optimized css', () => {
       ]
     );
 
-    assert.equal(auditResult.results.length, 2);
-    assert.equal(auditResult.results[0].url, 'foo.css');
-    assert.equal(Math.round(auditResult.results[0].wastedPercent), 65);
-    assert.equal(Math.round(auditResult.results[0].wastedBytes / 1024), 13);
-    assert.equal(auditResult.results[1].url, 'other.css');
-    assert.equal(Math.round(auditResult.results[1].wastedPercent), 8);
-    assert.equal(Math.round(auditResult.results[1].wastedBytes / 1024), 4);
+    assert.equal(auditResult.items.length, 2);
+    assert.equal(auditResult.items[0].url, 'foo.css');
+    assert.equal(Math.round(auditResult.items[0].wastedPercent), 65);
+    assert.equal(Math.round(auditResult.items[0].wastedBytes / 1024), 13);
+    assert.equal(auditResult.items[1].url, 'other.css');
+    assert.equal(Math.round(auditResult.items[1].wastedPercent), 8);
+    assert.equal(Math.round(auditResult.items[1].wastedBytes / 1024), 4);
   });
 
   it('passes when stylesheets are already minified', () => {
@@ -190,6 +190,6 @@ describe('Page uses optimized css', () => {
       ]
     );
 
-    assert.equal(auditResult.results.length, 0);
+    assert.equal(auditResult.items.length, 0);
   });
 });
diff --git a/lighthouse-core/test/audits/byte-efficiency/unminified-javascript-test.js b/lighthouse-core/test/audits/byte-efficiency/unminified-javascript-test.js
index 2b95c084796d..9905c3b49b57 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unminified-javascript-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unminified-javascript-test.js
@@ -53,16 +53,16 @@ describe('Page uses optimized responses', () => {
     ]);
 
     assert.ok(auditResult.warnings.length);
-    assert.equal(auditResult.results.length, 3);
-    assert.equal(auditResult.results[0].url, 'foo.js');
-    assert.equal(Math.round(auditResult.results[0].wastedPercent), 57);
-    assert.equal(Math.round(auditResult.results[0].wastedBytes / 1024), 11);
-    assert.equal(auditResult.results[1].url, 'other.js');
-    assert.equal(Math.round(auditResult.results[1].wastedPercent), 53);
-    assert.equal(Math.round(auditResult.results[1].wastedBytes / 1024), 27);
-    assert.equal(auditResult.results[2].url, 'valid-ish.js');
-    assert.equal(Math.round(auditResult.results[2].wastedPercent), 72);
-    assert.equal(Math.round(auditResult.results[2].wastedBytes / 1024), 72);
+    assert.equal(auditResult.items.length, 3);
+    assert.equal(auditResult.items[0].url, 'foo.js');
+    assert.equal(Math.round(auditResult.items[0].wastedPercent), 57);
+    assert.equal(Math.round(auditResult.items[0].wastedBytes / 1024), 11);
+    assert.equal(auditResult.items[1].url, 'other.js');
+    assert.equal(Math.round(auditResult.items[1].wastedPercent), 53);
+    assert.equal(Math.round(auditResult.items[1].wastedBytes / 1024), 27);
+    assert.equal(auditResult.items[2].url, 'valid-ish.js');
+    assert.equal(Math.round(auditResult.items[2].wastedPercent), 72);
+    assert.equal(Math.round(auditResult.items[2].wastedBytes / 1024), 72);
   });
 
   it('passes when scripts are already minified', () => {
@@ -89,6 +89,6 @@ describe('Page uses optimized responses', () => {
       {requestId: '123.3', url: 'invalid.js', _transferSize: 20 * KB, _resourceType},
     ]);
 
-    assert.equal(auditResult.results.length, 0);
+    assert.equal(auditResult.items.length, 0);
   });
 });
diff --git a/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js b/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
index 27b22d78e62d..4a77a8cf0e15 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
@@ -109,9 +109,9 @@ describe('Best Practices: unused css rules audit', () => {
     });
 
     it('correctly computes url', () => {
-      const expectedPreview = {type: 'code', value: 'dummy'};
-      assert.deepEqual(map({header: {sourceURL: ''}}).url, expectedPreview);
-      assert.deepEqual(map({header: {sourceURL: 'a'}}, 'http://g.co/a').url, expectedPreview);
+      const expectedPreview ='dummy';
+      assert.strictEqual(map({header: {sourceURL: ''}}).url, expectedPreview);
+      assert.strictEqual(map({header: {sourceURL: 'a'}}, 'http://g.co/a').url, expectedPreview);
       assert.equal(map({header: {sourceURL: 'foobar'}}).url, 'http://g.co/foobar');
     });
   });
@@ -135,7 +135,7 @@ describe('Best Practices: unused css rules audit', () => {
         URL: {finalUrl: ''},
         CSSUsage: {rules: [{styleSheetId: 'a', used: false}], stylesheets: []},
       }).then(result => {
-        assert.equal(result.results.length, 0);
+        assert.equal(result.items.length, 0);
       });
     });
 
@@ -159,7 +159,7 @@ describe('Best Practices: unused css rules audit', () => {
           },
         ]},
       }).then(result => {
-        assert.equal(result.results.length, 0);
+        assert.equal(result.items.length, 0);
       });
     });
 
@@ -186,11 +186,11 @@ describe('Best Practices: unused css rules audit', () => {
           },
         ]},
       }).then(result => {
-        assert.equal(result.results.length, 2);
-        assert.equal(result.results[0].totalBytes, 10 * 1024);
-        assert.equal(result.results[1].totalBytes, 6000);
-        assert.equal(result.results[0].wastedPercent, 75);
-        assert.equal(result.results[1].wastedPercent, 50);
+        assert.equal(result.items.length, 2);
+        assert.equal(result.items[0].totalBytes, 10 * 1024);
+        assert.equal(result.items[1].totalBytes, 6000);
+        assert.equal(result.items[0].wastedPercent, 75);
+        assert.equal(result.items[1].wastedPercent, 50);
       });
     });
 
@@ -225,8 +225,8 @@ describe('Best Practices: unused css rules audit', () => {
           },
         ]},
       }).then(result => {
-        assert.equal(result.results.length, 1);
-        assert.equal(Math.floor(result.results[0].wastedPercent), 33);
+        assert.equal(result.items.length, 1);
+        assert.equal(Math.floor(result.items[0].wastedPercent), 33);
       });
     });
   });
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 bc161dab8e34..68c6064e9340 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unused-javascript-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unused-javascript-test.js
@@ -86,14 +86,14 @@ describe('UnusedJavaScript audit', () => {
     );
 
     it('should merge duplicates', () => {
-      assert.equal(result.results.length, 2);
+      assert.equal(result.items.length, 2);
 
-      const scriptBWaste = result.results[0];
+      const scriptBWaste = result.items[0];
       assert.equal(scriptBWaste.totalBytes, 50000);
       assert.equal(scriptBWaste.wastedBytes, 12500);
       assert.equal(scriptBWaste.wastedPercent, 25);
 
-      const inlineWaste = result.results[1];
+      const inlineWaste = result.items[1];
       assert.equal(inlineWaste.totalBytes, 21000);
       assert.equal(inlineWaste.wastedBytes, 6000);
       assert.equal(Math.round(inlineWaste.wastedPercent), 29);
diff --git a/lighthouse-core/test/audits/byte-efficiency/uses-optimized-images-test.js b/lighthouse-core/test/audits/byte-efficiency/uses-optimized-images-test.js
index 793cd2362da3..625179993c3c 100644
--- a/lighthouse-core/test/audits/byte-efficiency/uses-optimized-images-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/uses-optimized-images-test.js
@@ -36,7 +36,7 @@ describe('Page uses optimized images', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 0);
+    assert.equal(auditResult.items.length, 0);
   });
 
   it('flags files when there is only small savings', () => {
@@ -46,7 +46,7 @@ describe('Page uses optimized images', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 1);
+    assert.equal(auditResult.items.length, 1);
   });
 
   it('ignores files when no jpeg savings is available', () => {
@@ -56,7 +56,7 @@ describe('Page uses optimized images', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 0);
+    assert.equal(auditResult.items.length, 0);
   });
 
   it('passes when all images are sufficiently optimized', () => {
@@ -70,7 +70,7 @@ describe('Page uses optimized images', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 0);
+    assert.equal(auditResult.items.length, 0);
   });
 
   it('limits output of data URIs', () => {
@@ -79,7 +79,7 @@ describe('Page uses optimized images', () => {
       OptimizedImages: [image],
     });
 
-    const actualUrl = auditResult.results[0].url;
+    const actualUrl = auditResult.items[0].url;
     assert.ok(actualUrl.length < image.url.length, `${actualUrl} >= ${image.url}`);
   });
 
diff --git a/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js b/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js
index c8380103da7c..ea8053f42e95 100644
--- a/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js
@@ -48,9 +48,9 @@ describe('Page uses responsive images', () => {
         ],
       });
 
-      assert.equal(result.results.length, data.listed ? 1 : 0);
+      assert.equal(result.items.length, data.listed ? 1 : 0);
       if (data.listed) {
-        assert.equal(Math.round(result.results[0].wastedBytes / 1024), data.expectedWaste);
+        assert.equal(Math.round(result.items[0].wastedBytes / 1024), data.expectedWaste);
       }
     });
   }
@@ -101,7 +101,7 @@ describe('Page uses responsive images', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 0);
+    assert.equal(auditResult.items.length, 0);
   });
 
   it('identifies when images are not wasteful', () => {
@@ -129,7 +129,7 @@ describe('Page uses responsive images', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 2);
+    assert.equal(auditResult.items.length, 2);
   });
 
   it('ignores vectors', () => {
@@ -144,7 +144,7 @@ describe('Page uses responsive images', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 0);
+    assert.equal(auditResult.items.length, 0);
   });
 
   it('de-dupes images', () => {
@@ -166,7 +166,7 @@ describe('Page uses responsive images', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 1);
-    assert.equal(auditResult.results[0].wastedPercent, 75, 'correctly computes wastedPercent');
+    assert.equal(auditResult.items.length, 1);
+    assert.equal(auditResult.items[0].wastedPercent, 75, 'correctly computes wastedPercent');
   });
 });
diff --git a/lighthouse-core/test/audits/byte-efficiency/uses-text-compression-test.js b/lighthouse-core/test/audits/byte-efficiency/uses-text-compression-test.js
index ae7319c6b8c0..f68fd75e284b 100644
--- a/lighthouse-core/test/audits/byte-efficiency/uses-text-compression-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/uses-text-compression-test.js
@@ -31,7 +31,7 @@ describe('Page uses optimized responses', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 2);
+    assert.equal(auditResult.items.length, 2);
   });
 
   it('passes when all responses are sufficiently optimized', () => {
@@ -45,6 +45,6 @@ describe('Page uses optimized responses', () => {
       ],
     });
 
-    assert.equal(auditResult.results.length, 1);
+    assert.equal(auditResult.items.length, 1);
   });
 });

From c155cccba92c911d4e91665e4b4fe165dbcf0c23 Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Tue, 22 May 2018 14:53:39 -0700
Subject: [PATCH 08/11] fix smoke tests

---
 .../byte-efficiency/expectations.js           | 38 ++++++-------------
 .../dobetterweb/dbw-expectations.js           |  4 +-
 2 files changed, 12 insertions(+), 30 deletions(-)

diff --git a/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js b/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js
index 755b69239c9b..885bc04729c9 100644
--- a/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js
+++ b/lighthouse-cli/test/smokehouse/byte-efficiency/expectations.js
@@ -18,9 +18,7 @@ module.exports = [
     audits: {
       'unminified-css': {
         details: {
-          summary: {
-            wastedBytes: '>17000',
-          },
+          overallSavingsBytes: '>17000',
           items: {
             length: 1,
           },
@@ -29,10 +27,8 @@ module.exports = [
       'unminified-javascript': {
         score: '<1',
         details: {
-          summary: {
-            wastedBytes: '>45000',
-            wastedMs: '>500',
-          },
+          overallSavingsBytes: '>45000',
+          overallSavingsMs: '>500',
           items: {
             length: 1,
           },
@@ -40,9 +36,7 @@ module.exports = [
       },
       'unused-css-rules': {
         details: {
-          summary: {
-            wastedBytes: '>39000',
-          },
+          overallSavingsBytes: '>39000',
           items: {
             length: 2,
           },
@@ -51,10 +45,8 @@ module.exports = [
       'unused-javascript': {
         score: '<1',
         details: {
-          summary: {
-            wastedBytes: '>=25000',
-            wastedMs: '>300',
-          },
+          overallSavingsBytes: '>=25000',
+          overallSavingsMs: '>300',
           items: {
             length: 2,
           },
@@ -77,9 +69,7 @@ module.exports = [
       },
       'uses-webp-images': {
         details: {
-          summary: {
-            wastedBytes: '>60000',
-          },
+          overallSavingsBytes: '>60000',
           items: {
             length: 4,
           },
@@ -88,10 +78,8 @@ module.exports = [
       'uses-text-compression': {
         score: '<1',
         details: {
-          summary: {
-            wastedMs: '>700',
-            wastedBytes: '>50000',
-          },
+          overallSavingsMs: '>700',
+          overallSavingsBytes: '>50000',
           items: {
             length: 2,
           },
@@ -99,9 +87,7 @@ module.exports = [
       },
       'uses-optimized-images': {
         details: {
-          summary: {
-            wastedBytes: '>10000',
-          },
+          overallSavingsBytes: '>10000',
           items: {
             length: 1,
           },
@@ -113,9 +99,7 @@ module.exports = [
           75,
         ],
         details: {
-          summary: {
-            wastedBytes: '>75000',
-          },
+          overallSavingsBytes: '>75000',
           items: [
             {wastedPercent: '<60'},
             {wastedPercent: '<60'},
diff --git a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
index 3722bb2386c2..910a3c62fb95 100644
--- a/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
+++ b/lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
@@ -135,9 +135,7 @@ module.exports = [
       'efficient-animated-content': {
         score: 0,
         details: {
-          summary: {
-            wastedMs: '>2000',
-          },
+          overallSavingsMs: '>2000',
           items: [
             {
               url: 'http://localhost:10200/dobetterweb/lighthouse-rotating.gif',

From 788ad28be163aab4dc81e06f7c991e1c0660fe66 Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Tue, 22 May 2018 14:54:35 -0700
Subject: [PATCH 09/11] fix opportunity byte granularity

---
 lighthouse-core/report/html/renderer/details-renderer.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index 456fd93cc8e9..7f5bea91ce48 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -299,7 +299,7 @@ class DetailsRenderer {
           }
           case 'bytes': {
             const numValue = /** @type {number} */ (value);
-            itemElement = this._renderBytes({value: numValue});
+            itemElement = this._renderBytes({value: numValue, granularity: 1});
             break;
           }
           case 'thumbnail': {

From 7c42100e072997fb9952f42b955bd9fad8686f5c Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Thu, 24 May 2018 12:30:41 -0700
Subject: [PATCH 10/11] more feedback

---
 lighthouse-core/audits/audit.js               |   2 +-
 .../report/html/renderer/details-renderer.js  |   2 +-
 .../byte-efficiency/unused-css-rules-test.js  |   2 +-
 lighthouse-core/test/results/sample_v2.json   | 149 +-----------------
 4 files changed, 11 insertions(+), 144 deletions(-)

diff --git a/lighthouse-core/audits/audit.js b/lighthouse-core/audits/audit.js
index 877cb36eb09e..178e42fd9763 100644
--- a/lighthouse-core/audits/audit.js
+++ b/lighthouse-core/audits/audit.js
@@ -135,7 +135,7 @@ class Audit {
   static makeOpportunityDetails(headings, items, overallSavingsMs, overallSavingsBytes) {
     return {
       type: 'opportunity',
-      headings,
+      headings: items.length === 0 ? [] : headings,
       items,
       overallSavingsMs,
       overallSavingsBytes,
diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index 7f5bea91ce48..58355feb1b7e 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -59,7 +59,7 @@ class DetailsRenderer {
         // @ts-ignore - TODO(bckenny): Fix type hierarchy
         return this._renderTable(/** @type {TableDetailsJSON} */ (details));
       case 'code':
-        return this._renderCode(/** @type {StringDetailsJSON} */ (details));
+        return this._renderCode(/** @type {DetailsJSON} */ (details));
       case 'node':
         return this.renderNode(/** @type {NodeDetailsJSON} */(details));
       case 'criticalrequestchain':
diff --git a/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js b/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
index 4a77a8cf0e15..4dc357859333 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
@@ -109,7 +109,7 @@ describe('Best Practices: unused css rules audit', () => {
     });
 
     it('correctly computes url', () => {
-      const expectedPreview ='dummy';
+      const expectedPreview = 'dummy';
       assert.strictEqual(map({header: {sourceURL: ''}}).url, expectedPreview);
       assert.strictEqual(map({header: {sourceURL: 'a'}}, 'http://g.co/a').url, expectedPreview);
       assert.equal(map({header: {sourceURL: 'foobar'}}).url, 'http://g.co/foobar');
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 0a353e490bb4..a7f4d5a66d12 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -788,18 +788,7 @@
       ],
       "details": {
         "type": "opportunity",
-        "headings": [
-          {
-            "key": "url",
-            "valueType": "url",
-            "label": "URL"
-          },
-          {
-            "key": "wastedMs",
-            "valueType": "timespanMs",
-            "label": "Potential Savings"
-          }
-        ],
+        "headings": [],
         "items": [],
         "overallSavingsMs": 0
       }
@@ -817,18 +806,7 @@
       ],
       "details": {
         "type": "opportunity",
-        "headings": [
-          {
-            "key": "url",
-            "valueType": "url",
-            "label": "Origin"
-          },
-          {
-            "key": "wastedMs",
-            "valueType": "timespanMs",
-            "label": "Potential Savings"
-          }
-        ],
+        "headings": [],
         "items": [],
         "overallSavingsMs": 0
       }
@@ -1881,28 +1859,7 @@
       "warnings": [],
       "details": {
         "type": "opportunity",
-        "headings": [
-          {
-            "key": "url",
-            "valueType": "thumbnail",
-            "label": ""
-          },
-          {
-            "key": "url",
-            "valueType": "url",
-            "label": "URL"
-          },
-          {
-            "key": "totalBytes",
-            "valueType": "bytes",
-            "label": "Original"
-          },
-          {
-            "key": "wastedBytes",
-            "valueType": "bytes",
-            "label": "Potential Savings"
-          }
-        ],
+        "headings": [],
         "items": [],
         "overallSavingsMs": 0,
         "overallSavingsBytes": 0
@@ -1975,23 +1932,7 @@
       "displayValue": "",
       "details": {
         "type": "opportunity",
-        "headings": [
-          {
-            "key": "url",
-            "valueType": "url",
-            "label": "URL"
-          },
-          {
-            "key": "totalBytes",
-            "valueType": "bytes",
-            "label": "Original"
-          },
-          {
-            "key": "wastedBytes",
-            "valueType": "bytes",
-            "label": "Potential Savings"
-          }
-        ],
+        "headings": [],
         "items": [],
         "overallSavingsMs": 0,
         "overallSavingsBytes": 0
@@ -2050,23 +1991,7 @@
       "displayValue": "",
       "details": {
         "type": "opportunity",
-        "headings": [
-          {
-            "key": "url",
-            "valueType": "url",
-            "label": "URL"
-          },
-          {
-            "key": "totalBytes",
-            "valueType": "bytes",
-            "label": "Original"
-          },
-          {
-            "key": "wastedBytes",
-            "valueType": "bytes",
-            "label": "Potential Savings"
-          }
-        ],
+        "headings": [],
         "items": [],
         "overallSavingsMs": 0,
         "overallSavingsBytes": 0
@@ -2132,28 +2057,7 @@
       "warnings": [],
       "details": {
         "type": "opportunity",
-        "headings": [
-          {
-            "key": "url",
-            "valueType": "thumbnail",
-            "label": ""
-          },
-          {
-            "key": "url",
-            "valueType": "url",
-            "label": "URL"
-          },
-          {
-            "key": "totalBytes",
-            "valueType": "bytes",
-            "label": "Original"
-          },
-          {
-            "key": "wastedBytes",
-            "valueType": "bytes",
-            "label": "Potential Savings"
-          }
-        ],
+        "headings": [],
         "items": [],
         "overallSavingsMs": 0,
         "overallSavingsBytes": 0
@@ -2216,28 +2120,7 @@
       "warnings": [],
       "details": {
         "type": "opportunity",
-        "headings": [
-          {
-            "key": "url",
-            "valueType": "thumbnail",
-            "label": ""
-          },
-          {
-            "key": "url",
-            "valueType": "url",
-            "label": "URL"
-          },
-          {
-            "key": "totalBytes",
-            "valueType": "bytes",
-            "label": "Original"
-          },
-          {
-            "key": "wastedBytes",
-            "valueType": "bytes",
-            "label": "Potential Savings"
-          }
-        ],
+        "headings": [],
         "items": [],
         "overallSavingsMs": 0,
         "overallSavingsBytes": 0
@@ -2253,23 +2136,7 @@
       "displayValue": "",
       "details": {
         "type": "opportunity",
-        "headings": [
-          {
-            "key": "url",
-            "valueType": "url",
-            "label": "URL"
-          },
-          {
-            "key": "totalBytes",
-            "valueType": "bytes",
-            "label": "Transfer Size"
-          },
-          {
-            "key": "wastedBytes",
-            "valueType": "bytes",
-            "label": "Byte Savings"
-          }
-        ],
+        "headings": [],
         "items": [],
         "overallSavingsMs": 0,
         "overallSavingsBytes": 0

From f4ff217244ccdcd7fe30860915be2d99965763e0 Mon Sep 17 00:00:00 2001
From: Brendan Kenny 
Date: Thu, 24 May 2018 14:16:43 -0700
Subject: [PATCH 11/11] add TODO

---
 lighthouse-core/report/html/renderer/details-renderer.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index 58355feb1b7e..7f59c8e8bac5 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -280,6 +280,7 @@ class DetailsRenderer {
         const valueType = heading.valueType;
         let itemElement;
 
+        // TODO(bckenny): as we add more table types, split out into _renderTableItem fn.
         switch (valueType) {
           case 'url': {
             // Fall back to 
 rendering if not actually a URL.