Repository: GoogleChrome/lighthouse
Pull request #10564 (2020-04-09 01:53:02)

core(legacy-javascript): use source maps
Part of #10369

If source maps are present, use our knowledge of what node module a polyfill comes from to find signals. Pattern matching takes precedence - the line/column information is more accurate. Otherwise, the first mapping entry for the relevant module is used as the source location.

Because the case of no source maps is important by itself, I introduced a second summary json file in the test.

Diff between the two new summary jsons (This represents the improvement in using maps + pattern matching) (same as what github shows for summary-signals.json in this PR, but this markdown is a bit nicer to look at) :
```diff
diff --git a/summary-signals-nomaps.json b/summary-signals.json
index 690bd7b27..022109f5e 100644
--- a/summary-signals-nomaps.json
+++ b/summary-signals.json
@@ -1,51 +1,13 @@
 {
-  "totalSignals": 203,
+  "totalSignals": 251,
   "variantsMissingSignals": [
-    "core-js-2-only-polyfill/es6-array-filter",
-    "core-js-2-only-polyfill/es6-array-map",
-    "core-js-2-only-polyfill/es6-array-reduce",
-    "core-js-2-only-polyfill/es6-array-reduce-right",
-    "core-js-2-only-polyfill/es6-array-some",
-    "core-js-2-only-polyfill/es6-date-to-string",
-    "core-js-2-only-polyfill/es6-function-name",
-    "core-js-2-only-polyfill/es6-object-freeze",
-    "core-js-2-only-polyfill/es6-object-get-own-property-descriptor",
-    "core-js-2-only-polyfill/es6-object-get-own-property-names",
-    "core-js-2-only-polyfill/es6-object-get-prototype-of",
-    "core-js-2-only-polyfill/es6-object-is-extensible",
-    "core-js-2-only-polyfill/es6-object-is-frozen",
-    "core-js-2-only-polyfill/es6-object-is-sealed",
-    "core-js-2-only-polyfill/es6-object-keys",
-    "core-js-2-only-polyfill/es6-object-prevent-extensions",
-    "core-js-2-only-polyfill/es6-object-seal",
-    "core-js-2-only-polyfill/es6-reflect-define-property",
-    "core-js-2-only-polyfill/es6-string-trim",
     "core-js-2-preset-env-esmodules/true",
-    "core-js-3-only-polyfill/es6-array-filter",
-    "core-js-3-only-polyfill/es6-array-map",
-    "core-js-3-only-polyfill/es6-array-reduce",
-    "core-js-3-only-polyfill/es6-array-reduce-right",
-    "core-js-3-only-polyfill/es6-array-some",
-    "core-js-3-only-polyfill/es6-date-to-string",
-    "core-js-3-only-polyfill/es6-function-name",
-    "core-js-3-only-polyfill/es6-object-freeze",
-    "core-js-3-only-polyfill/es6-object-get-own-property-descriptor",
-    "core-js-3-only-polyfill/es6-object-get-own-property-names",
-    "core-js-3-only-polyfill/es6-object-get-prototype-of",
-    "core-js-3-only-polyfill/es6-object-is-extensible",
-    "core-js-3-only-polyfill/es6-object-is-frozen",
-    "core-js-3-only-polyfill/es6-object-is-sealed",
-    "core-js-3-only-polyfill/es6-object-keys",
-    "core-js-3-only-polyfill/es6-object-prevent-extensions",
-    "core-js-3-only-polyfill/es6-object-seal",
-    "core-js-3-only-polyfill/es6-reflect-define-property",
-    "core-js-3-only-polyfill/es6-string-trim",
     "core-js-3-preset-env-esmodules/true"
   ],
   "variants": [
     {
       "name": "core-js-2-only-polyfill/es6-array-filter",
-      "signals": ""
+      "signals": "Array.prototype.filter"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-find",
@@ -53,7 +15,7 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-find-index",
-      "signals": "Array.prototype.findIndex"
+      "signals": "Array.prototype.findIndex, Array.prototype.find"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-for-each",
@@ -73,7 +35,7 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-map",
-      "signals": ""
+      "signals": "Array.prototype.map"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-of",
@@ -81,15 +43,15 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-reduce",
-      "signals": ""
+      "signals": "Array.prototype.reduce"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-reduce-right",
-      "signals": ""
+      "signals": "Array.prototype.reduce, Array.prototype.reduceRight"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-some",
-      "signals": ""
+      "signals": "Array.prototype.some"
     },
     {
       "name": "core-js-2-only-polyfill/es6-date-now",
@@ -105,11 +67,11 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-date-to-string",
-      "signals": ""
+      "signals": "Date.prototype.toString"
     },
     {
       "name": "core-js-2-only-polyfill/es6-function-name",
-      "signals": ""
+      "signals": "Function.prototype.name"
     },
     {
       "name": "core-js-2-only-polyfill/es6-map",
@@ -149,43 +111,43 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-freeze",
-      "signals": ""
+      "signals": "Object.freeze"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-get-own-property-descriptor",
-      "signals": ""
+      "signals": "Object.getOwnPropertyDescriptor"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-get-own-property-names",
-      "signals": ""
+      "signals": "Object.getOwnPropertyNames"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-get-prototype-of",
-      "signals": ""
+      "signals": "Object.getPrototypeOf"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-is-extensible",
-      "signals": ""
+      "signals": "Object.isExtensible"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-is-frozen",
-      "signals": ""
+      "signals": "Object.isFrozen"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-is-sealed",
-      "signals": ""
+      "signals": "Object.isSealed"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-keys",
-      "signals": ""
+      "signals": "Object.keys"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-prevent-extensions",
-      "signals": ""
+      "signals": "Object.preventExtensions"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-seal",
-      "signals": ""
+      "signals": "Object.seal"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-set-prototype-of",
@@ -205,7 +167,7 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-reflect-define-property",
-      "signals": ""
+      "signals": "Reflect.defineProperty"
     },
     {
       "name": "core-js-2-only-polyfill/es6-reflect-delete-property",
@@ -217,11 +179,11 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-reflect-get-own-property-descriptor",
-      "signals": "Reflect.getOwnPropertyDescriptor"
+      "signals": "Reflect.getOwnPropertyDescriptor, Reflect.get"
     },
     {
       "name": "core-js-2-only-polyfill/es6-reflect-get-prototype-of",
-      "signals": "Reflect.getPrototypeOf"
+      "signals": "Reflect.getPrototypeOf, Reflect.get"
     },
     {
       "name": "core-js-2-only-polyfill/es6-reflect-has",
@@ -245,7 +207,7 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-reflect-set-prototype-of",
-      "signals": "Reflect.setPrototypeOf"
+      "signals": "Reflect.setPrototypeOf, Reflect.set"
     },
     {
       "name": "core-js-2-only-polyfill/es6-set",
@@ -281,7 +243,7 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-string-trim",
-      "signals": ""
+      "signals": "String.prototype.trim"
     },
     {
       "name": "core-js-2-only-polyfill/es6-typed-array-buffer",
@@ -369,7 +331,7 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-filter",
-      "signals": ""
+      "signals": "Array.prototype.filter"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-find",
@@ -377,7 +339,7 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-find-index",
-      "signals": "Array.prototype.findIndex"
+      "signals": "Array.prototype.findIndex, Array.prototype.find"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-for-each",
@@ -397,7 +359,7 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-map",
-      "signals": ""
+      "signals": "Array.prototype.map"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-of",
@@ -405,15 +367,15 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-reduce",
-      "signals": ""
+      "signals": "Array.prototype.reduce"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-reduce-right",
-      "signals": ""
+      "signals": "Array.prototype.reduce, Array.prototype.reduceRight"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-some",
-      "signals": ""
+      "signals": "Array.prototype.some"
     },
     {
       "name": "core-js-3-only-polyfill/es6-date-now",
@@ -429,11 +391,11 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-date-to-string",
-      "signals": ""
+      "signals": "Date.prototype.toString"
     },
     {
       "name": "core-js-3-only-polyfill/es6-function-name",
-      "signals": ""
+      "signals": "Function.prototype.name"
     },
     {
       "name": "core-js-3-only-polyfill/es6-map",
@@ -473,43 +435,43 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-freeze",
-      "signals": ""
+      "signals": "Object.freeze"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-get-own-property-descriptor",
-      "signals": ""
+      "signals": "Object.getOwnPropertyDescriptor"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-get-own-property-names",
-      "signals": ""
+      "signals": "Object.getOwnPropertyNames"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-get-prototype-of",
-      "signals": ""
+      "signals": "Object.getPrototypeOf"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-is-extensible",
-      "signals": ""
+      "signals": "Object.isExtensible"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-is-frozen",
-      "signals": ""
+      "signals": "Object.isFrozen"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-is-sealed",
-      "signals": ""
+      "signals": "Object.isSealed"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-keys",
-      "signals": ""
+      "signals": "Object.keys"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-prevent-extensions",
-      "signals": ""
+      "signals": "Object.preventExtensions"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-seal",
-      "signals": ""
+      "signals": "Object.seal"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-set-prototype-of",
@@ -529,7 +491,7 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-reflect-define-property",
-      "signals": ""
+      "signals": "Reflect.defineProperty"
     },
     {
       "name": "core-js-3-only-polyfill/es6-reflect-delete-property",
@@ -541,11 +503,11 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-reflect-get-own-property-descriptor",
-      "signals": "Reflect.getOwnPropertyDescriptor"
+      "signals": "Reflect.getOwnPropertyDescriptor, Reflect.get"
     },
     {
       "name": "core-js-3-only-polyfill/es6-reflect-get-prototype-of",
-      "signals": "Reflect.getPrototypeOf"
+      "signals": "Reflect.getPrototypeOf, Reflect.get"
     },
     {
       "name": "core-js-3-only-polyfill/es6-reflect-has",
@@ -569,7 +531,7 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-reflect-set-prototype-of",
-      "signals": "Reflect.setPrototypeOf"
+      "signals": "Reflect.setPrototypeOf, Reflect.set"
     },
     {
       "name": "core-js-3-only-polyfill/es6-set",
@@ -605,7 +567,7 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-string-trim",
-      "signals": ""
+      "signals": "String.prototype.trim"
     },
     {
       "name": "core-js-3-only-polyfill/es6-typed-array-buffer",

```

connorjclark: @patrickhulce  bump :)
connorjclark: I'm not bothering to look until #10566 lands, maybe moot.
patrickhulce: I'm a little unclear what the status is here @connorjclark. Marked as waiting on me so presumably the blockers were cleared, but the linked issue was an issue not a PR (but I guess *its linked* PR landed).

Are the current build failures expected? The only substantial change I see since my last review is the `endsWith` change I have an open question on :)

Patch:
diff --git a/docs/performance-budgets.md b/docs/performance-budgets.md
index 619fcc8a0..4eb3fea71 100644
--- a/docs/performance-budgets.md
+++ b/docs/performance-budgets.md
@@ -97,6 +97,8 @@ Supported timing metrics:
 - `estimated-input-latency`
 - `total-blocking-time`
 - `speed-index`
+- `largest-contentful-paint`
+- `cumulative-layout-shift`
 
 ### Resource Budgets
 
diff --git a/lighthouse-core/audits/byte-efficiency/unused-javascript.js b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
index 7ce637d91..675bcab62 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-javascript.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
@@ -6,7 +6,8 @@
 'use strict';
 
 const ByteEfficiencyAudit = require('./byte-efficiency-audit.js');
-const JSBundles = require('../../computed/js-bundles.js');
+const UnusedJavaScriptSummary = require('../../computed/unused-javascript-summary.js');
+const JsBundles = require('../../computed/js-bundles.js');
 const i18n = require('../../lib/i18n/i18n.js');
 
 const UIStrings = {
@@ -71,151 +72,6 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
     };
   }
 
-  /**
-   * @param {LH.Crdp.Profiler.ScriptCoverage} scriptCoverage
-   * @return {WasteData}
-   */
-  static computeWaste(scriptCoverage) {
-    let maximumEndOffset = 0;
-    for (const func of scriptCoverage.functions) {
-      maximumEndOffset = Math.max(maximumEndOffset, ...func.ranges.map(r => r.endOffset));
-    }
-
-    // We only care about unused ranges of the script, so we can ignore all the nesting and safely
-    // assume that if a range is unexecuted, all nested ranges within it will also be unexecuted.
-    const unusedByIndex = new Uint8Array(maximumEndOffset);
-    for (const func of scriptCoverage.functions) {
-      for (const range of func.ranges) {
-        if (range.count === 0) {
-          for (let i = range.startOffset; i < range.endOffset; i++) {
-            unusedByIndex[i] = 1;
-          }
-        }
-      }
-    }
-
-    let unused = 0;
-    for (const x of unusedByIndex) {
-      unused += x;
-    }
-
-    return {
-      unusedByIndex,
-      unusedLength: unused,
-      contentLength: maximumEndOffset,
-    };
-  }
-
-  /**
-   * @param {LH.Audit.ByteEfficiencyItem} item
-   * @param {WasteData[]} wasteData
-   * @param {LH.Artifacts.Bundle} bundle
-   * @param {ReturnType<typeof UnusedJavaScript.determineLengths>} lengths
-   * @param {number} bundleSourceUnusedThreshold
-   */
-  static createBundleMultiData(item, wasteData, bundle, lengths, bundleSourceUnusedThreshold) {
-    if (!bundle.script.content) return;
-
-    /** @type {Record<string, number>} */
-    const files = {};
-
-    const lineLengths = bundle.script.content.split('\n').map(l => l.length);
-    let totalSoFar = 0;
-    const lineOffsets = lineLengths.map(len => {
-      const retVal = totalSoFar;
-      totalSoFar += len + 1;
-      return retVal;
-    });
-
-    // @ts-ignore: We will upstream computeLastGeneratedColumns to CDT eventually.
-    bundle.map.computeLastGeneratedColumns();
-    for (const mapping of bundle.map.mappings()) {
-      let offset = lineOffsets[mapping.lineNumber];
-
-      offset += mapping.columnNumber;
-      const lastColumnOfMapping =
-        // @ts-ignore: We will upstream lastColumnNumber to CDT eventually.
-        (mapping.lastColumnNumber - 1) || lineLengths[mapping.lineNumber];
-      for (let i = mapping.columnNumber; i <= lastColumnOfMapping; i++) {
-        if (wasteData.every(data => data.unusedByIndex[offset] === 1)) {
-          const key = mapping.sourceURL || '(unmapped)';
-          files[key] = (files[key] || 0) + 1;
-        }
-        offset += 1;
-      }
-    }
-
-    const transferRatio = lengths.transfer / lengths.content;
-    const topUnusedFilesSizes = Object.entries(files)
-      .filter(([_, unusedBytes]) => unusedBytes * transferRatio >= bundleSourceUnusedThreshold)
-      .sort(([_, unusedBytes1], [__, unusedBytes2]) => unusedBytes2 - unusedBytes1)
-      .slice(0, 5)
-      .map(([key, unusedBytes]) => {
-        const total = key === '(unmapped)' ? bundle.sizes.unmappedBytes : bundle.sizes.files[key];
-        return {
-          key,
-          unused: Math.round(unusedBytes * transferRatio),
-          total: Math.round(total * transferRatio),
-        };
-      });
-
-    const commonSourcePrefix = commonPrefix([...bundle.map._sourceInfos.keys()]);
-    Object.assign(item, {
-      sources: trimCommonPrefix(topUnusedFilesSizes.map(d => d.key), commonSourcePrefix),
-      sourceBytes: topUnusedFilesSizes.map(d => d.total),
-      sourceWastedBytes: topUnusedFilesSizes.map(d => d.unused),
-    });
-  }
-
-  /**
-   * @param {WasteData[]} wasteData
-   * @param {string} url
-   * @param {ReturnType<typeof UnusedJavaScript.determineLengths>} lengths
-   * @return {LH.Audit.ByteEfficiencyItem}
-   */
-  static mergeWaste(wasteData, url, lengths) {
-    let unused = 0;
-    let content = 0;
-    // TODO: this is right for multiple script tags in an HTML document,
-    // but may be wrong for multiple frames using the same script resource.
-    for (const usage of wasteData) {
-      unused += usage.unusedLength;
-      content += usage.contentLength;
-    }
-
-    const wastedRatio = (unused / content) || 0;
-    const wastedBytes = Math.round(lengths.transfer * wastedRatio);
-
-    return {
-      url: url,
-      totalBytes: lengths.transfer,
-      wastedBytes,
-      wastedPercent: 100 * wastedRatio,
-    };
-  }
-
-  /**
-   * @param {WasteData[]} wasteData
-   * @param {LH.Artifacts.NetworkRequest} networkRecord
-   */
-  static determineLengths(wasteData, networkRecord) {
-    let unused = 0;
-    let content = 0;
-    // TODO: this is right for multiple script tags in an HTML document,
-    // but may be wrong for multiple frames using the same script resource.
-    for (const usage of wasteData) {
-      unused += usage.unusedLength;
-      content += usage.contentLength;
-    }
-    const transfer = ByteEfficiencyAudit.estimateTransferSize(networkRecord, content, 'Script');
-
-    return {
-      content,
-      unused,
-      transfer,
-    };
-  }
-
   /**
    * @param {LH.Artifacts} artifacts
    * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
@@ -223,31 +79,54 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
    * @return {Promise<ByteEfficiencyAudit.ByteEfficiencyProduct>}
    */
   static async audit_(artifacts, networkRecords, context) {
-    const bundles = artifacts.SourceMaps ? await JSBundles.request(artifacts, context) : [];
+    const bundles = await JsBundles.request(artifacts, context);
     const {bundleSourceUnusedThreshold = IGNORE_BUNDLE_SOURCE_THRESHOLD_IN_BYTES} =
       context.options || {};
 
-    /** @type {Map<string, Array<LH.Crdp.Profiler.ScriptCoverage>>} */
-    const scriptsByUrl = new Map();
-    for (const script of artifacts.JsUsage) {
-      const scripts = scriptsByUrl.get(script.url) || [];
-      scripts.push(script);
-      scriptsByUrl.set(script.url, scripts);
-    }
-
     const items = [];
-    for (const [url, scriptCoverage] of scriptsByUrl.entries()) {
+    for (const [url, scriptCoverages] of Object.entries(artifacts.JsUsage)) {
       const networkRecord = networkRecords.find(record => record.url === url);
       if (!networkRecord) continue;
-      const wasteData = scriptCoverage.map(UnusedJavaScript.computeWaste);
-      const lengths = UnusedJavaScript.determineLengths(wasteData, networkRecord);
       const bundle = bundles.find(b => b.script.src === url);
-      const item = UnusedJavaScript.mergeWaste(wasteData, networkRecord.url, lengths);
+      const unusedJsSummary =
+        await UnusedJavaScriptSummary.request({url, scriptCoverages, bundle}, context);
+
+      const transfer = ByteEfficiencyAudit
+        .estimateTransferSize(networkRecord, unusedJsSummary.totalBytes, 'Script');
+      const transferRatio = transfer / unusedJsSummary.totalBytes;
+      const item = {
+        url: unusedJsSummary.url,
+        totalBytes: Math.round(transferRatio * unusedJsSummary.totalBytes),
+        wastedBytes: Math.round(transferRatio * unusedJsSummary.wastedBytes),
+        wastedPercent: unusedJsSummary.wastedPercent,
+      };
+
       if (item.wastedBytes <= IGNORE_THRESHOLD_IN_BYTES) continue;
-      if (bundle) {
-        UnusedJavaScript.createBundleMultiData(
-          item, wasteData, bundle, lengths, bundleSourceUnusedThreshold);
+
+      // Augment with bundle data.
+      if (bundle && unusedJsSummary.sourcesWastedBytes) {
+        const topUnusedSourceSizes = Object.entries(unusedJsSummary.sourcesWastedBytes)
+          .sort((a, b) => b[1] - a[1])
+          .slice(0, 5)
+          .map(([source, unused]) => {
+            const total =
+              source === '(unmapped)' ? bundle.sizes.unmappedBytes : bundle.sizes.files[source];
+            return {
+              source,
+              unused: Math.round(unused * transferRatio),
+              total: Math.round(total * transferRatio),
+            };
+          })
+          .filter(d => d.unused >= bundleSourceUnusedThreshold);
+
+        const commonSourcePrefix = commonPrefix([...bundle.map._sourceInfos.keys()]);
+        Object.assign(item, {
+          sources: trimCommonPrefix(topUnusedSourceSizes.map(d => d.source), commonSourcePrefix),
+          sourceBytes: topUnusedSourceSizes.map(d => d.total),
+          sourceWastedBytes: topUnusedSourceSizes.map(d => d.unused),
+        });
       }
+
       items.push(item);
     }
 
diff --git a/lighthouse-core/audits/legacy-javascript.js b/lighthouse-core/audits/legacy-javascript.js
index 0f1ed78a4..62d5eb242 100644
--- a/lighthouse-core/audits/legacy-javascript.js
+++ b/lighthouse-core/audits/legacy-javascript.js
@@ -18,6 +18,7 @@
 const Audit = require('./audit.js');
 const NetworkRecords = require('../computed/network-records.js');
 const MainResource = require('../computed/main-resource.js');
+const JSBundles = require('../computed/js-bundles.js');
 const URL = require('../lib/url-shim.js');
 const i18n = require('../lib/i18n/i18n.js');
 
@@ -107,7 +108,7 @@ class LegacyJavascript extends Audit {
       scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
       description: str_(UIStrings.description),
       title: str_(UIStrings.title),
-      requiredArtifacts: ['devtoolsLogs', 'ScriptElements', 'URL'],
+      requiredArtifacts: ['devtoolsLogs', 'ScriptElements', 'SourceMaps', 'URL'],
     };
   }
 
@@ -159,97 +160,103 @@ class LegacyJavascript extends Audit {
     return expression;
   }
 
+  static getPolyfillData() {
+    return [
+      /* eslint-disable max-len */
+      {module: 'es6.array.fill', name: 'Array.prototype.fill'},
+      {module: 'es6.array.filter', name: 'Array.prototype.filter'},
+      {module: 'es6.array.find', name: 'Array.prototype.find'},
+      {module: 'es6.array.find-index', name: 'Array.prototype.findIndex'},
+      {module: 'es6.array.for-each', name: 'Array.prototype.forEach'},
+      {module: 'es6.array.from', name: 'Array.from'},
+      {module: 'es6.array.is-array', name: 'Array.isArray'},
+      {module: 'es6.array.last-index-of', name: 'Array.prototype.lastIndexOf'},
+      {module: 'es6.array.map', name: 'Array.prototype.map'},
+      {module: 'es6.array.of', name: 'Array.of'},
+      {module: 'es6.array.reduce', name: 'Array.prototype.reduce'},
+      {module: 'es6.array.reduce-right', name: 'Array.prototype.reduceRight'},
+      {module: 'es6.array.some', name: 'Array.prototype.some'},
+      {module: 'es6.date.now', name: 'Date.now'},
+      {module: 'es6.date.to-iso-string', name: 'Date.prototype.toISOString'},
+      {module: 'es6.date.to-json', name: 'Date.prototype.toJSON'},
+      {module: 'es6.date.to-string', name: 'Date.prototype.toString'},
+      {module: 'es6.function.name', name: 'Function.prototype.name'},
+      {module: 'es6.map', name: 'Map'},
+      {module: 'es6.number.is-integer', name: 'Number.isInteger'},
+      {module: 'es6.number.is-safe-integer', name: 'Number.isSafeInteger'},
+      {module: 'es6.number.parse-float', name: 'Number.parseFloat'},
+      {module: 'es6.number.parse-int', name: 'Number.parseInt'},
+      {module: 'es6.object.assign', name: 'Object.assign'},
+      {module: 'es6.object.create', name: 'Object.create'},
+      {module: 'es6.object.define-properties', name: 'Object.defineProperties'},
+      {module: 'es6.object.define-property', name: 'Object.defineProperty'},
+      {module: 'es6.object.freeze', name: 'Object.freeze'},
+      {module: 'es6.object.get-own-property-descriptor', name: 'Object.getOwnPropertyDescriptor'},
+      {module: 'es6.object.get-own-property-names', name: 'Object.getOwnPropertyNames'},
+      {module: 'es6.object.get-prototype-of', name: 'Object.getPrototypeOf'},
+      {module: 'es6.object.is-extensible', name: 'Object.isExtensible'},
+      {module: 'es6.object.is-frozen', name: 'Object.isFrozen'},
+      {module: 'es6.object.is-sealed', name: 'Object.isSealed'},
+      {module: 'es6.object.keys', name: 'Object.keys'},
+      {module: 'es6.object.prevent-extensions', name: 'Object.preventExtensions'},
+      {module: 'es6.object.seal', name: 'Object.seal'},
+      {module: 'es6.object.set-prototype-of', name: 'Object.setPrototypeOf'},
+      {module: 'es6.promise', name: 'Promise'},
+      {module: 'es6.reflect.apply', name: 'Reflect.apply'},
+      {module: 'es6.reflect.construct', name: 'Reflect.construct'},
+      {module: 'es6.reflect.define-property', name: 'Reflect.defineProperty'},
+      {module: 'es6.reflect.delete-property', name: 'Reflect.deleteProperty'},
+      {module: 'es6.reflect.get', name: 'Reflect.get'},
+      {module: 'es6.reflect.get-own-property-descriptor', name: 'Reflect.getOwnPropertyDescriptor'},
+      {module: 'es6.reflect.get-prototype-of', name: 'Reflect.getPrototypeOf'},
+      {module: 'es6.reflect.has', name: 'Reflect.has'},
+      {module: 'es6.reflect.is-extensible', name: 'Reflect.isExtensible'},
+      {module: 'es6.reflect.own-keys', name: 'Reflect.ownKeys'},
+      {module: 'es6.reflect.prevent-extensions', name: 'Reflect.preventExtensions'},
+      {module: 'es6.reflect.set', name: 'Reflect.set'},
+      {module: 'es6.reflect.set-prototype-of', name: 'Reflect.setPrototypeOf'},
+      {module: 'es6.set', name: 'Set'},
+      {module: 'es6.string.code-point-at', name: 'String.prototype.codePointAt'},
+      {module: 'es6.string.ends-with', name: 'String.prototype.endsWith'},
+      {module: 'es6.string.from-code-point', name: 'String.fromCodePoint'},
+      {module: 'es6.string.includes', name: 'String.prototype.includes'},
+      {module: 'es6.string.raw', name: 'String.raw'},
+      {module: 'es6.string.repeat', name: 'String.prototype.repeat'},
+      {module: 'es6.string.starts-with', name: 'String.prototype.startsWith'},
+      {module: 'es6.string.trim', name: 'String.prototype.trim'},
+      {module: 'es6.typed.array-buffer', name: 'ArrayBuffer'},
+      {module: 'es6.typed.data-view', name: 'DataView'},
+      {module: 'es6.typed.float32-array', name: 'Float32Array'},
+      {module: 'es6.typed.float64-array', name: 'Float64Array'},
+      {module: 'es6.typed.int16-array', name: 'Int16Array'},
+      {module: 'es6.typed.int32-array', name: 'Int32Array'},
+      {module: 'es6.typed.int8-array', name: 'Int8Array'},
+      {module: 'es6.typed.uint16-array', name: 'Uint16Array'},
+      {module: 'es6.typed.uint32-array', name: 'Uint32Array'},
+      {module: 'es6.typed.uint8-array', name: 'Uint8Array'},
+      {module: 'es6.typed.uint8-clamped-array', name: 'Uint8ClampedArray'},
+      {module: 'es6.weak-map', name: 'WeakMap'},
+      {module: 'es6.weak-set', name: 'WeakSet'},
+      {module: 'es7.array.includes', name: 'Array.prototype.includes'},
+      {module: 'es7.object.entries', name: 'Object.entries'},
+      {module: 'es7.object.get-own-property-descriptors', name: 'Object.getOwnPropertyDescriptors'},
+      {module: 'es7.object.values', name: 'Object.values'},
+      {module: 'es7.string.pad-end', name: 'String.prototype.padEnd'},
+      {module: 'es7.string.pad-start', name: 'String.prototype.padStart'},
+      /* eslint-enable max-len */
+    ];
+  }
+
   /**
    * @return {Pattern[]}
    */
   static getPolyfillPatterns() {
-    return [
-      'Array.fill',
-      'Array.from',
-      'Array.isArray',
-      'Array.of',
-      'Array.prototype.filter',
-      'Array.prototype.find',
-      'Array.prototype.findIndex',
-      'Array.prototype.forEach',
-      'Array.prototype.includes',
-      'Array.prototype.lastIndexOf',
-      'Array.prototype.map',
-      'Array.prototype.reduce',
-      'Array.prototype.reduceRight',
-      'Array.prototype.some',
-      'ArrayBuffer',
-      'DataView',
-      'Date.now',
-      'Date.prototype.toISOString',
-      'Date.prototype.toJSON',
-      'Date.prototype.toString',
-      'Float32Array',
-      'Float64Array',
-      'Function.prototype.name',
-      'Int16Array',
-      'Int32Array',
-      'Int8Array',
-      'Map',
-      'Number.isInteger',
-      'Number.isSafeInteger',
-      'Number.parseFloat',
-      'Number.parseInt',
-      'Object.assign',
-      'Object.create',
-      'Object.defineProperties',
-      'Object.defineProperty',
-      'Object.entries',
-      'Object.freeze',
-      'Object.getOwnPropertyDescriptor',
-      'Object.getOwnPropertyDescriptors',
-      'Object.getOwnPropertyNames',
-      'Object.getPrototypeOf',
-      'Object.isExtensible',
-      'Object.isFrozen',
-      'Object.isSealed',
-      'Object.keys',
-      'Object.preventExtensions',
-      'Object.seal',
-      'Object.setPrototypeOf',
-      'Object.values',
-      'Promise',
-      'Reflect.apply',
-      'Reflect.construct',
-      'Reflect.defineProperty',
-      'Reflect.deleteProperty',
-      'Reflect.get',
-      'Reflect.getOwnPropertyDescriptor',
-      'Reflect.getPrototypeOf',
-      'Reflect.has',
-      'Reflect.isExtensible',
-      'Reflect.ownKeys',
-      'Reflect.preventExtensions',
-      'Reflect.set',
-      'Reflect.setPrototypeOf',
-      'Set',
-      'String.fromCodePoint',
-      'String.prototype.codePointAt',
-      'String.prototype.endsWith',
-      'String.prototype.includes',
-      'String.prototype.padEnd',
-      'String.prototype.padStart',
-      'String.prototype.repeat',
-      'String.prototype.startsWith',
-      'String.prototype.trim',
-      'String.raw',
-      'Uint16Array',
-      'Uint32Array',
-      'Uint8Array',
-      'Uint8ClampedArray',
-      'WeakMap',
-      'WeakSet',
-    ].map(polyfillName => {
-      const parts = polyfillName.split('.');
+    return this.getPolyfillData().map(({name}) => {
+      const parts = name.split('.');
       const object = parts.length > 1 ? parts.slice(0, parts.length - 1).join('.') : null;
       const property = parts[parts.length - 1];
       return {
-        name: polyfillName,
+        name,
         expression: this.buildPolyfillExpression(object, property),
       };
     });
@@ -276,23 +283,46 @@ class LegacyJavascript extends Audit {
   }
 
   /**
-   * Returns a collection of match results grouped by script url and with a mapping
-   * to determine the order in which the matches were discovered.
+   * Returns a collection of match results grouped by script url.
    *
    * @param {CodePatternMatcher} matcher
    * @param {LH.GathererArtifacts['ScriptElements']} scripts
    * @param {LH.Artifacts.NetworkRequest[]} networkRecords
+   * @param {LH.Artifacts.Bundle[]} bundles
    * @return {Map<string, PatternMatchResult[]>}
    */
-  static detectCodePatternsAcrossScripts(matcher, scripts, networkRecords) {
+  static detectAcrossScripts(matcher, scripts, networkRecords, bundles) {
     /** @type {Map<string, PatternMatchResult[]>} */
     const urlToMatchResults = new Map();
+    const polyfillData = this.getPolyfillData();
 
     for (const {requestId, content} of Object.values(scripts)) {
       if (!content) continue;
       const networkRecord = networkRecords.find(record => record.requestId === requestId);
       if (!networkRecord) continue;
+
+      // Start with pattern matching against the downloaded script.
       const matches = matcher.match(content);
+
+      // If it's a bundle with source maps, add in the polyfill modules by name too.
+      const bundle = bundles.find(b => b.script.src === networkRecord.url);
+      if (bundle) {
+        for (const {module, name} of polyfillData) {
+          // Skip if the pattern matching found a match for this polyfill.
+          if (matches.some(m => m.name === name)) continue;
+
+          const source = bundle.rawMap.sources.find(source => source.endsWith(`${module}.js`));
+          if (!source) continue;
+
+          const mapping = bundle.map.mappings().find(m => m.sourceURL === source);
+          if (mapping) {
+            matches.push({name, line: mapping.lineNumber, column: mapping.columnNumber});
+          } else {
+            matches.push({name, line: 0, column: 0});
+          }
+        }
+      }
+
       if (!matches.length) continue;
       urlToMatchResults.set(networkRecord.url, matches);
     }
@@ -312,6 +342,7 @@ class LegacyJavascript extends Audit {
       URL: artifacts.URL,
       devtoolsLog,
     }, context);
+    const bundles = await JSBundles.request(artifacts, context);
 
     /** @type {Array<{url: string, signals: string[], locations: LH.Audit.Details.SourceLocationValue[]}>} */
     const tableRows = [];
@@ -324,7 +355,7 @@ class LegacyJavascript extends Audit {
     ]);
 
     const urlToMatchResults =
-      this.detectCodePatternsAcrossScripts(matcher, artifacts.ScriptElements, networkRecords);
+      this.detectAcrossScripts(matcher, artifacts.ScriptElements, networkRecords, bundles);
     urlToMatchResults.forEach((matches, url) => {
       /** @type {typeof tableRows[number]} */
       const row = {url, signals: [], locations: []};
diff --git a/lighthouse-core/audits/metrics/cumulative-layout-shift.js b/lighthouse-core/audits/metrics/cumulative-layout-shift.js
index 37dfed97b..bd360d1cd 100644
--- a/lighthouse-core/audits/metrics/cumulative-layout-shift.js
+++ b/lighthouse-core/audits/metrics/cumulative-layout-shift.js
@@ -10,8 +10,6 @@ const ComputedCLS = require('../../computed/metrics/cumulative-layout-shift.js')
 const i18n = require('../../lib/i18n/i18n.js');
 
 const UIStrings = {
-  /** The name of the metric "Cumulative Layout Shift" that indicates how much the page changes its layout while it loads. If big segments of the page shift their location during load, the Cumulative Layout Shift will be higher. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
-  title: 'Cumulative Layout Shift',
   /** Description of the Cumulative Layout Shift metric that indicates how much the page changes its layout while it loads. If big segments of the page shift their location during load, the Cumulative Layout Shift will be higher. This description is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
   description: 'Cumulative Layout Shift is the sum of all layout shifts that occurred during a ' +
       'page\'s load. A layout shift is any movement an element makes once it is visible to the ' +
@@ -32,7 +30,7 @@ class CumulativeLayoutShift extends Audit {
   static get meta() {
     return {
       id: 'cumulative-layout-shift',
-      title: str_(UIStrings.title),
+      title: str_(i18n.UIStrings.cumulativeLayoutShiftMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
       requiredArtifacts: ['traces'],
diff --git a/lighthouse-core/audits/metrics/largest-contentful-paint.js b/lighthouse-core/audits/metrics/largest-contentful-paint.js
index a2af0829a..770aa2262 100644
--- a/lighthouse-core/audits/metrics/largest-contentful-paint.js
+++ b/lighthouse-core/audits/metrics/largest-contentful-paint.js
@@ -10,8 +10,6 @@ const i18n = require('../../lib/i18n/i18n.js');
 const ComputedLcp = require('../../computed/metrics/largest-contentful-paint.js');
 
 const UIStrings = {
-  /** The name of the metric that marks the time at which the largest text or image is painted by the browser. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
-  title: 'Largest Contentful Paint',
   /** Description of the Largest Contentful Paint (LCP) metric, which marks the time at which the largest text or image is painted by the browser. This is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
   description: 'Largest Contentful Paint marks the time at which the largest text or image is ' +
       `painted. [Learn More](https://web.dev/lighthouse-largest-contentful-paint)`,
@@ -26,7 +24,7 @@ class LargestContentfulPaint extends Audit {
   static get meta() {
     return {
       id: 'largest-contentful-paint',
-      title: str_(UIStrings.title),
+      title: str_(i18n.UIStrings.largestContentfulPaintMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
       requiredArtifacts: ['traces', 'devtoolsLogs'],
diff --git a/lighthouse-core/audits/timing-budget.js b/lighthouse-core/audits/timing-budget.js
index cb1367fae..927f352f6 100644
--- a/lighthouse-core/audits/timing-budget.js
+++ b/lighthouse-core/audits/timing-budget.js
@@ -55,6 +55,8 @@ class TimingBudget extends Audit {
       'estimated-input-latency': i18n.UIStrings.estimatedInputLatencyMetric,
       'total-blocking-time': i18n.UIStrings.totalBlockingTimeMetric,
       'speed-index': i18n.UIStrings.speedIndexMetric,
+      'largest-contentful-paint': i18n.UIStrings.largestContentfulPaintMetric,
+      'cumulative-layout-shift': i18n.UIStrings.cumulativeLayoutShiftMetric,
     };
     return str_(strMappings[timingMetric]);
   }
@@ -75,6 +77,8 @@ class TimingBudget extends Audit {
       'estimated-input-latency': summary.estimatedInputLatency,
       'total-blocking-time': summary.totalBlockingTime,
       'speed-index': summary.speedIndex,
+      'largest-contentful-paint': summary.largestContentfulPaint,
+      'cumulative-layout-shift': summary.cumulativeLayoutShift,
     };
     return measurements[timingMetric];
   }
diff --git a/lighthouse-core/computed/unused-javascript-summary.js b/lighthouse-core/computed/unused-javascript-summary.js
new file mode 100644
index 000000000..74467b16c
--- /dev/null
+++ b/lighthouse-core/computed/unused-javascript-summary.js
@@ -0,0 +1,172 @@
+/**
+ * @license Copyright 2020 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const makeComputedArtifact = require('./computed-artifact.js');
+
+/**
+ * @typedef WasteData
+ * @property {Uint8Array} unusedByIndex
+ * @property {number} unusedLength
+ * @property {number} contentLength
+ */
+
+/**
+ * @typedef ComputeInput
+ * @property {string} url
+ * @property {LH.Crdp.Profiler.ScriptCoverage[]} scriptCoverages
+ * @property {LH.Artifacts.Bundle=} bundle
+ */
+
+/**
+ * @typedef Summary
+ * @property {string} url
+ * @property {number} wastedBytes
+ * @property {number} totalBytes
+ * @property {number} wastedBytes
+ * @property {number=} wastedPercent
+ * @property {Record<string, number>=} sourcesWastedBytes
+ */
+
+class UnusedJavascriptSummary {
+  /**
+   * @param {LH.Crdp.Profiler.ScriptCoverage} scriptCoverage
+   * @return {WasteData}
+   */
+  static computeWaste(scriptCoverage) {
+    let maximumEndOffset = 0;
+    for (const func of scriptCoverage.functions) {
+      maximumEndOffset = Math.max(maximumEndOffset, ...func.ranges.map(r => r.endOffset));
+    }
+
+    // We only care about unused ranges of the script, so we can ignore all the nesting and safely
+    // assume that if a range is unexecuted, all nested ranges within it will also be unexecuted.
+    const unusedByIndex = new Uint8Array(maximumEndOffset);
+    for (const func of scriptCoverage.functions) {
+      for (const range of func.ranges) {
+        if (range.count === 0) {
+          for (let i = range.startOffset; i < range.endOffset; i++) {
+            unusedByIndex[i] = 1;
+          }
+        }
+      }
+    }
+
+    let unused = 0;
+    for (const x of unusedByIndex) {
+      unused += x;
+    }
+
+    return {
+      unusedByIndex,
+      unusedLength: unused,
+      contentLength: maximumEndOffset,
+    };
+  }
+
+  /**
+   * @param {string} url
+   * @param {ReturnType<typeof UnusedJavascriptSummary.determineLengths>} lengths
+   * @return {Summary}
+   */
+  static createItem(url, lengths) {
+    const wastedRatio = (lengths.unused / lengths.content) || 0;
+    const wastedBytes = Math.round(lengths.content * wastedRatio);
+
+    return {
+      url: url,
+      totalBytes: lengths.content,
+      wastedBytes,
+      wastedPercent: 100 * wastedRatio,
+    };
+  }
+
+  /**
+   * @param {WasteData[]} wasteData
+   */
+  static determineLengths(wasteData) {
+    let unused = 0;
+    let content = 0;
+    // TODO: this is right for multiple script tags in an HTML document,
+    // but may be wrong for multiple frames using the same script resource.
+    for (const usage of wasteData) {
+      unused += usage.unusedLength;
+      content += usage.contentLength;
+    }
+
+    return {
+      content,
+      unused,
+    };
+  }
+
+  /**
+   * @param {WasteData[]} wasteData
+   * @param {LH.Artifacts.Bundle} bundle
+   */
+  static createSourceWastedBytes(wasteData, bundle) {
+    if (!bundle.script.content) return;
+
+    /** @type {Record<string, number>} */
+    const files = {};
+
+    const lineLengths = bundle.script.content.split('\n').map(l => l.length);
+    let totalSoFar = 0;
+    const lineOffsets = lineLengths.map(len => {
+      const retVal = totalSoFar;
+      totalSoFar += len + 1;
+      return retVal;
+    });
+
+    // @ts-ignore: We will upstream computeLastGeneratedColumns to CDT eventually.
+    bundle.map.computeLastGeneratedColumns();
+    for (const mapping of bundle.map.mappings()) {
+      let offset = lineOffsets[mapping.lineNumber];
+
+      offset += mapping.columnNumber;
+      const lastColumnOfMapping =
+        // @ts-ignore: We will upstream lastColumnNumber to CDT eventually.
+        (mapping.lastColumnNumber - 1) || lineLengths[mapping.lineNumber];
+      for (let i = mapping.columnNumber; i <= lastColumnOfMapping; i++) {
+        if (wasteData.every(data => data.unusedByIndex[offset] === 1)) {
+          const key = mapping.sourceURL || '(unmapped)';
+          files[key] = (files[key] || 0) + 1;
+        }
+        offset += 1;
+      }
+    }
+
+    const dataSorted = Object.entries(files)
+      .sort(([_, unusedBytes1], [__, unusedBytes2]) => unusedBytes2 - unusedBytes1);
+
+    /** @type {Record<string, number>} */
+    const bundleData = {};
+    for (const [key, unusedBytes] of dataSorted) {
+      bundleData[key] = unusedBytes;
+    }
+    return bundleData;
+  }
+
+  /**
+   * @param {ComputeInput} data
+   * @return {Promise<Summary>}
+   */
+  static async compute_(data) {
+    const {url, scriptCoverages, bundle} = data;
+
+    const wasteData = scriptCoverages.map(UnusedJavascriptSummary.computeWaste);
+    const lengths = UnusedJavascriptSummary.determineLengths(wasteData);
+    const item = UnusedJavascriptSummary.createItem(url, lengths);
+    if (!bundle) return item;
+
+    return {
+      ...item,
+      sourcesWastedBytes: UnusedJavascriptSummary.createSourceWastedBytes(wasteData, bundle),
+    };
+  }
+}
+
+module.exports = makeComputedArtifact(UnusedJavascriptSummary);
diff --git a/lighthouse-core/config/budget.js b/lighthouse-core/config/budget.js
index bc61e9d3d..e4f51212b 100644
--- a/lighthouse-core/config/budget.js
+++ b/lighthouse-core/config/budget.js
@@ -225,6 +225,9 @@ class Budget {
       'max-potential-fid',
       'estimated-input-latency',
       'total-blocking-time',
+      'speed-index',
+      'largest-contentful-paint',
+      'cumulative-layout-shift',
     ];
     // Assume metric is an allowed string, throw if not.
     if (!validTimingMetrics.includes(/** @type {LH.Budget.TimingMetric} */ (metric))) {
diff --git a/lighthouse-core/gather/gatherers/js-usage.js b/lighthouse-core/gather/gatherers/js-usage.js
index c513f9eb1..25aceeb2e 100644
--- a/lighthouse-core/gather/gatherers/js-usage.js
+++ b/lighthouse-core/gather/gatherers/js-usage.js
@@ -27,9 +27,19 @@ class JsUsage extends Gatherer {
     const driver = passContext.driver;
 
     const coverageResponse = await driver.sendCommand('Profiler.takePreciseCoverage');
+    const scriptUsages = coverageResponse.result;
     await driver.sendCommand('Profiler.stopPreciseCoverage');
     await driver.sendCommand('Profiler.disable');
-    return coverageResponse.result;
+
+    /** @type {Record<string, Array<LH.Crdp.Profiler.ScriptCoverage>>} */
+    const usageByUrl = {};
+    for (const scriptUsage of scriptUsages) {
+      const scripts = usageByUrl[scriptUsage.url] || [];
+      scripts.push(scriptUsage);
+      usageByUrl[scriptUsage.url] = scripts;
+    }
+
+    return usageByUrl;
   }
 }
 
diff --git a/lighthouse-core/lib/i18n/i18n.js b/lighthouse-core/lib/i18n/i18n.js
index 959177b54..dd3d58cf3 100644
--- a/lighthouse-core/lib/i18n/i18n.js
+++ b/lighthouse-core/lib/i18n/i18n.js
@@ -116,6 +116,10 @@ const UIStrings = {
   maxPotentialFIDMetric: 'Max Potential First Input Delay',
   /** The name of the metric that summarizes how quickly the page looked visually complete. The name of this metric is largely abstract and can be loosely translated. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
   speedIndexMetric: 'Speed Index',
+  /** The name of the metric that marks the time at which the largest text or image is painted by the browser. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
+  largestContentfulPaintMetric: 'Largest Contentful Paint',
+  /** The name of the metric "Cumulative Layout Shift" that indicates how much the page changes its layout while it loads. If big segments of the page shift their location during load, the Cumulative Layout Shift will be higher. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
+  cumulativeLayoutShiftMetric: 'Cumulative Layout Shift',
 };
 
 const formats = {
diff --git a/lighthouse-core/lib/i18n/locales/ar-XB.json b/lighthouse-core/lib/i18n/locales/ar-XB.json
index 63ad040c1..2d347a882 100644
--- a/lighthouse-core/lib/i18n/locales/ar-XB.json
+++ b/lighthouse-core/lib/i18n/locales/ar-XB.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "‏‮Largest‬‏ ‏‮Contentful‬‏ ‏‮Paint‬‏ ‏‮marks‬‏ ‏‮the‬‏ ‏‮time‬‏ ‏‮at‬‏ ‏‮which‬‏ ‏‮the‬‏ ‏‮largest‬‏ ‏‮text‬‏ ‏‮or‬‏ ‏‮image‬‏ ‏‮is‬‏ ‏‮painted‬‏. [‏‮Learn‬‏ ‏‮More‬‏](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "‏‮Largest‬‏ ‏‮Contentful‬‏ ‏‮Paint‬‏"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "‏‮The‬‏ ‏‮maximum‬‏ ‏‮potential‬‏ ‏‮First‬‏ ‏‮Input‬‏ ‏‮Delay‬‏ ‏‮that‬‏ ‏‮your‬‏ ‏‮users‬‏ ‏‮could‬‏ ‏‮experience‬‏ ‏‮is‬‏ ‏‮the‬‏ ‏‮duration‬‏ ‏‮of‬‏ ‏‮the‬‏ ‏‮longest‬‏ ‏‮task‬‏. [‏‮Learn‬‏ ‏‮more‬‏](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ar.json b/lighthouse-core/lib/i18n/locales/ar.json
index d3ba9bddf..1bee7f30a 100644
--- a/lighthouse-core/lib/i18n/locales/ar.json
+++ b/lighthouse-core/lib/i18n/locales/ar.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "تحدد \"سرعة عرض أكبر محتوى على الصفحة\" الوقت الذي يُعرَض فيه أكبر صورة أو نص. [مزيد من المعلومات](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "سرعة عرض أكبر محتوى"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "الحد الأقصى المحتمل من \"مهلة الاستجابة لأوّل إدخال\" الذي قد يواجهه المستخدمين، هو مدة أطول مهمّة. [مزيد من المعلومات](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/bg.json b/lighthouse-core/lib/i18n/locales/bg.json
index 0e2006ab2..c3adcead7 100644
--- a/lighthouse-core/lib/i18n/locales/bg.json
+++ b/lighthouse-core/lib/i18n/locales/bg.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "„Изобразяване на най-голямото съдържание“ показва времето, когато се изобразяват най-големите текст или изображение. [Научете повече](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Изобразяване на най-голямото съдържание"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Максималното потенциално забавяне при първото взаимодействие на потребителите е продължителността на най-времеемката задача. [Научете повече](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ca.json b/lighthouse-core/lib/i18n/locales/ca.json
index a3c787dc6..22eebd162 100644
--- a/lighthouse-core/lib/i18n/locales/ca.json
+++ b/lighthouse-core/lib/i18n/locales/ca.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "La mètrica Renderització de contingut més gran marca el moment en què es renderitza el text o la imatge més gran. [Obtén més informació](https://web.dev/largest-contentful-paint)."
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Renderització de contingut més gran"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "El retard potencial màxim respecte a la primera interacció que els usuaris es poden trobar és la durada de la tasca més llarga. [Obtén més informació](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/cs.json b/lighthouse-core/lib/i18n/locales/cs.json
index 8408d97d6..50ac7a3e3 100644
--- a/lighthouse-core/lib/i18n/locales/cs.json
+++ b/lighthouse-core/lib/i18n/locales/cs.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Vykreslení největšího obsahu udává čas, kdy byl vykreslen největší text nebo obrázek. [Další informace](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Vykreslení největšího obsahu"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Maximální potenciální prodleva prvního vstupu, kterou by uživatelé mohli pocítit, je trvání nejdelší úlohy. [Další informace](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/da.json b/lighthouse-core/lib/i18n/locales/da.json
index e80bb8e28..e8b499d75 100644
--- a/lighthouse-core/lib/i18n/locales/da.json
+++ b/lighthouse-core/lib/i18n/locales/da.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Største udfyldning af indhold angiver det tidspunkt, hvor den største tekst eller det største billede blev anvendt. [Få flere oplysninger](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Største udfyldning af indhold"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Den maksimale potentielle ventetid efter første input, som dine brugere kan opleve, er varigheden af den længste proces. [Få flere oplysninger](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/de.json b/lighthouse-core/lib/i18n/locales/de.json
index c0c26a1c3..eb6e20617 100644
--- a/lighthouse-core/lib/i18n/locales/de.json
+++ b/lighthouse-core/lib/i18n/locales/de.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Largest Contentful Paint gibt an, wann der längste Text bzw. das größte Bild gezeichnet wird. [Weitere Informationen](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Das maximale potenzielle First Input Delay, das bei Ihren Nutzern auftreten kann, entspricht der Dauer der längsten Aufgabe. [Weitere Informationen](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/el.json b/lighthouse-core/lib/i18n/locales/el.json
index 2d8be1184..322e716cc 100644
--- a/lighthouse-core/lib/i18n/locales/el.json
+++ b/lighthouse-core/lib/i18n/locales/el.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Η μέτρηση Μεγαλύτερη μορφή με περιεχόμενο, σηματοδοτεί τη χρονική στιγμή στην οποία σχεδιάζεται το μεγαλύτερο κείμενο ή εικόνα. [Μάθετε περισσότερα](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Μεγαλύτερη μορφή με περιεχόμενο"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Η μέγιστη δυνητική καθυστέρηση πρώτης εισόδου που θα μπορούσαν να αντιμετωπίσουν οι χρήστες είναι η διάρκεια της πιο χρονοβόρας εργασίας. [Μάθετε περισσότερα](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-GB.json b/lighthouse-core/lib/i18n/locales/en-GB.json
index 239128555..cb1f7f306 100644
--- a/lighthouse-core/lib/i18n/locales/en-GB.json
+++ b/lighthouse-core/lib/i18n/locales/en-GB.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn more](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "The maximum potential first input delay that your users could experience is the duration of the longest task. [Learn more](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json
index 8e64a615c..1e3619559 100644
--- a/lighthouse-core/lib/i18n/locales/en-US.json
+++ b/lighthouse-core/lib/i18n/locales/en-US.json
@@ -878,9 +878,6 @@
   "lighthouse-core/audits/metrics/cumulative-layout-shift.js | description": {
     "message": "Cumulative Layout Shift is the sum of all layout shifts that occurred during a page's load. A layout shift is any movement an element makes once it is visible to the user. All layout shift is recorded, scored, and then aggregated into a cumulative score between 0 and 1; 0 being a perfectly stable page, and >=0.5 being a highly shifting page. [Learn more](https://web.dev/cls)."
   },
-  "lighthouse-core/audits/metrics/cumulative-layout-shift.js | title": {
-    "message": "Cumulative Layout Shift"
-  },
   "lighthouse-core/audits/metrics/estimated-input-latency.js | description": {
     "message": "Estimated Input Latency is an estimate of how long your app takes to respond to user input, in milliseconds, during the busiest 5s window of page load. If your latency is higher than 50 ms, users may perceive your app as laggy. [Learn more](https://web.dev/estimated-input-latency)."
   },
@@ -899,9 +896,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn More](https://web.dev/lighthouse-largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "The maximum potential First Input Delay that your users could experience is the duration of the longest task. [Learn more](https://web.dev/lighthouse-max-potential-fid)."
   },
@@ -1463,6 +1457,9 @@
   "lighthouse-core/lib/i18n/i18n.js | columnWastedMs": {
     "message": "Potential Savings"
   },
+  "lighthouse-core/lib/i18n/i18n.js | cumulativeLayoutShiftMetric": {
+    "message": "Cumulative Layout Shift"
+  },
   "lighthouse-core/lib/i18n/i18n.js | displayValueByteSavings": {
     "message": "Potential savings of {wastedBytes, number, bytes} KB"
   },
@@ -1493,6 +1490,9 @@
   "lighthouse-core/lib/i18n/i18n.js | interactiveMetric": {
     "message": "Time to Interactive"
   },
+  "lighthouse-core/lib/i18n/i18n.js | largestContentfulPaintMetric": {
+    "message": "Largest Contentful Paint"
+  },
   "lighthouse-core/lib/i18n/i18n.js | maxPotentialFIDMetric": {
     "message": "Max Potential First Input Delay"
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-XA.json b/lighthouse-core/lib/i18n/locales/en-XA.json
index 31c0eae0d..4980ee6e8 100644
--- a/lighthouse-core/lib/i18n/locales/en-XA.json
+++ b/lighthouse-core/lib/i18n/locales/en-XA.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "[Ļåŕĝéšţ Çöñţéñţƒûļ Þåîñţ måŕķš ţĥé ţîmé åţ ŵĥîçĥ ţĥé ļåŕĝéšţ ţéxţ öŕ îmåĝé îš þåîñţéð. ᐅ[ᐊĻéåŕñ Möŕéᐅ](https://web.dev/largest-contentful-paint)ᐊ one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen]"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "[Ļåŕĝéšţ Çöñţéñţƒûļ Þåîñţ one two three]"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "[Ţĥé måxîmûm þöţéñţîåļ Fîŕšţ Îñþûţ Ðéļåý ţĥåţ ýöûŕ ûšéŕš çöûļð éxþéŕîéñçé îš ţĥé ðûŕåţîöñ öƒ ţĥé ļöñĝéšţ ţåšķ. ᐅ[ᐊĻéåŕñ möŕéᐅ](https://web.dev/lighthouse-max-potential-fid)ᐊ. one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty]"
   },
diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json
index 3e07156bd..7cbf1dcc9 100644
--- a/lighthouse-core/lib/i18n/locales/en-XL.json
+++ b/lighthouse-core/lib/i18n/locales/en-XL.json
@@ -878,9 +878,6 @@
   "lighthouse-core/audits/metrics/cumulative-layout-shift.js | description": {
     "message": "Ĉúm̂úl̂át̂ív̂é L̂áŷóût́ Ŝh́îf́t̂ íŝ t́ĥé ŝúm̂ óf̂ ál̂ĺ l̂áŷóût́ ŝh́îf́t̂ś t̂h́ât́ ôćĉúr̂ŕêd́ d̂úr̂ín̂ǵ â ṕâǵê'ś l̂óâd́. Â ĺâýôút̂ śĥíf̂t́ îś âńŷ ḿôv́êḿêńt̂ án̂ él̂ém̂én̂t́ m̂ák̂éŝ ón̂ćê ít̂ íŝ v́îśîb́l̂é t̂ó t̂h́ê úŝér̂. Ál̂ĺ l̂áŷóût́ ŝh́îf́t̂ íŝ ŕêćôŕd̂éd̂, śĉór̂éd̂, án̂d́ t̂h́êń âǵĝŕêǵât́êd́ îńt̂ó â ćûḿûĺât́îv́ê śĉór̂é b̂ét̂ẃêén̂ 0 án̂d́ 1; 0 b̂éîńĝ á p̂ér̂f́êćt̂ĺŷ śt̂áb̂ĺê ṕâǵê, án̂d́ >=0.5 b̂éîńĝ á ĥíĝh́l̂ý ŝh́îf́t̂ín̂ǵ p̂áĝé. [L̂éâŕn̂ ḿôŕê](https://web.dev/cls)."
   },
-  "lighthouse-core/audits/metrics/cumulative-layout-shift.js | title": {
-    "message": "Ĉúm̂úl̂át̂ív̂é L̂áŷóût́ Ŝh́îf́t̂"
-  },
   "lighthouse-core/audits/metrics/estimated-input-latency.js | description": {
     "message": "Êśt̂ím̂át̂éd̂ Ín̂ṕût́ L̂át̂én̂ćŷ íŝ án̂ éŝt́îḿât́ê óf̂ h́ôẃ l̂ón̂ǵ ŷóûŕ âṕp̂ t́âḱêś t̂ó r̂éŝṕôńd̂ t́ô úŝér̂ ín̂ṕût́, îń m̂íl̂ĺîśêćôńd̂ś, d̂úr̂ín̂ǵ t̂h́ê b́ûśîéŝt́ 5ŝ ẃîńd̂óŵ óf̂ ṕâǵê ĺôád̂. Íf̂ ýôúr̂ ĺât́êńĉý îś ĥíĝh́êŕ t̂h́âń 50 m̂ś, ûśêŕŝ ḿâý p̂ér̂ćêív̂é ŷóûŕ âṕp̂ áŝ ĺâǵĝý. [L̂éâŕn̂ ḿôŕê](https://web.dev/estimated-input-latency)."
   },
@@ -899,9 +896,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "L̂ár̂ǵêśt̂ Ćôńt̂én̂t́f̂úl̂ Ṕâín̂t́ m̂ár̂ḱŝ t́ĥé t̂ím̂é ât́ ŵh́îćĥ t́ĥé l̂ár̂ǵêśt̂ t́êx́t̂ ór̂ ím̂áĝé îś p̂áîńt̂éd̂. [Ĺêár̂ń M̂ór̂é](https://web.dev/lighthouse-largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "L̂ár̂ǵêśt̂ Ćôńt̂én̂t́f̂úl̂ Ṕâín̂t́"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "T̂h́ê ḿâx́îḿûḿ p̂ót̂én̂t́îál̂ F́îŕŝt́ Îńp̂út̂ D́êĺâý t̂h́ât́ ŷóûŕ ûśêŕŝ ćôúl̂d́ êx́p̂ér̂íêńĉé îś t̂h́ê d́ûŕât́îón̂ óf̂ t́ĥé l̂ón̂ǵêśt̂ t́âśk̂. [Ĺêár̂ń m̂ór̂é](https://web.dev/lighthouse-max-potential-fid)."
   },
@@ -1463,6 +1457,9 @@
   "lighthouse-core/lib/i18n/i18n.js | columnWastedMs": {
     "message": "P̂ót̂én̂t́îál̂ Śâv́îńĝś"
   },
+  "lighthouse-core/lib/i18n/i18n.js | cumulativeLayoutShiftMetric": {
+    "message": "Ĉúm̂úl̂át̂ív̂é L̂áŷóût́ Ŝh́îf́t̂"
+  },
   "lighthouse-core/lib/i18n/i18n.js | displayValueByteSavings": {
     "message": "P̂ót̂én̂t́îál̂ śâv́îńĝś ôf́ {wastedBytes, number, bytes} K̂B́"
   },
@@ -1493,6 +1490,9 @@
   "lighthouse-core/lib/i18n/i18n.js | interactiveMetric": {
     "message": "T̂ím̂é t̂ó Îńt̂ér̂áĉt́îv́ê"
   },
+  "lighthouse-core/lib/i18n/i18n.js | largestContentfulPaintMetric": {
+    "message": "L̂ár̂ǵêśt̂ Ćôńt̂én̂t́f̂úl̂ Ṕâín̂t́"
+  },
   "lighthouse-core/lib/i18n/i18n.js | maxPotentialFIDMetric": {
     "message": "M̂áx̂ Ṕôt́êńt̂íâĺ F̂ír̂śt̂ Ín̂ṕût́ D̂él̂áŷ"
   },
diff --git a/lighthouse-core/lib/i18n/locales/es-419.json b/lighthouse-core/lib/i18n/locales/es-419.json
index b57d100b7..fb5772dd4 100644
--- a/lighthouse-core/lib/i18n/locales/es-419.json
+++ b/lighthouse-core/lib/i18n/locales/es-419.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "La métrica \"Pintura más grande con contenido\" indica el momento en que se pinta el texto o imagen más grandes. [Obtén más información](https://web.dev/largest-contentful-paint)."
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Pintura más grande con contenido"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "El máximo retraso de primera entrada que podrían experimentar los usuarios es la duración de la tarea más larga. [Obtén más información](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/es.json b/lighthouse-core/lib/i18n/locales/es.json
index 4e1fa03c7..5cb6580d3 100644
--- a/lighthouse-core/lib/i18n/locales/es.json
+++ b/lighthouse-core/lib/i18n/locales/es.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "La métrica Largest Contentful Paint indica el tiempo que se tarda en dibujar el texto o la imagen de mayor tamaño. [Más información](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "La latencia potencial máxima de la primera interacción que podrían experimentar los usuarios es la duración de la tarea más larga. [Más información](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/fi.json b/lighthouse-core/lib/i18n/locales/fi.json
index 5e0de7ccb..af187dbf1 100644
--- a/lighthouse-core/lib/i18n/locales/fi.json
+++ b/lighthouse-core/lib/i18n/locales/fi.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Suurimman sisällön renderöinti mittaa suurimman tekstikohteen tai kuvan renderöintiaikaa. [Lue lisää](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Suurimman sisällön renderöinti"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Käyttäjien ensitoiminnon suurin mahdollinen viive on sama kuin pisimmän tehtävän kesto. [Lue lisää](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/fil.json b/lighthouse-core/lib/i18n/locales/fil.json
index 3679d3b87..d32adf6e1 100644
--- a/lighthouse-core/lib/i18n/locales/fil.json
+++ b/lighthouse-core/lib/i18n/locales/fil.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Minamarkahan ng Largest Contentful Paint ang tagal bago ma-paint ang pinakamalaking text o larawan. [Matuto Pa](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Ang maximum na potensyal na First Input Delay na puwedeng maranasan ng iyong mga user ay ang tagal ng pinakamahabang gawain. [Matuto pa](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/fr.json b/lighthouse-core/lib/i18n/locales/fr.json
index 3ca4dccaa..ab7ad94b0 100644
--- a/lighthouse-core/lib/i18n/locales/fr.json
+++ b/lighthouse-core/lib/i18n/locales/fr.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "La statistique \"Largest Contentful Paint\" indique le moment où le texte le plus long ou l'image la plus grande sont affichés. [En savoir plus](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Le retard maximal (Maximum Potential First Input Delay) auquel vos utilisateurs peuvent éventuellement être confrontés correspond à la durée de la tâche la plus longue. [En savoir plus](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/he.json b/lighthouse-core/lib/i18n/locales/he.json
index abcf9ca8f..4beab3084 100644
--- a/lighthouse-core/lib/i18n/locales/he.json
+++ b/lighthouse-core/lib/i18n/locales/he.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "הצגת חלק התוכן הגדול ביותר (LCP) מציינת את הזמן שבו התמונה או הטקסט הגדולים ביותר מוצגים. [מידע נוסף](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "הצגת חלק התוכן הגדול ביותר (LCP)"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "ההשהיה הפוטנציאלית המרבית שהמשתמשים יכולים לחוות לאחר קלט ראשוני (FID) היא משך הזמן של המשימה הארוכה ביותר. [מידע נוסף](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/hi.json b/lighthouse-core/lib/i18n/locales/hi.json
index b444ed161..33b6ac2e0 100644
--- a/lighthouse-core/lib/i18n/locales/hi.json
+++ b/lighthouse-core/lib/i18n/locales/hi.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn More](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "The maximum potential First Input Delay that your users could experience is the duration of the longest task. [Learn more](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/hr.json b/lighthouse-core/lib/i18n/locales/hr.json
index 75407ffd8..ed4f65481 100644
--- a/lighthouse-core/lib/i18n/locales/hr.json
+++ b/lighthouse-core/lib/i18n/locales/hr.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Najveće renderiranje sadržaja označava vrijeme renderiranja najvećeg teksta ili slike. [Saznajte više](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Najveće renderiranje sadržaja"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Maksimalno potencijalno kašnjenje odgovora na prvi unos koje bi korisnik mogao doživjeti trajanje je najduljeg zadatka. [Saznajte više](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/hu.json b/lighthouse-core/lib/i18n/locales/hu.json
index 96b039012..5bbbf1dd5 100644
--- a/lighthouse-core/lib/i18n/locales/hu.json
+++ b/lighthouse-core/lib/i18n/locales/hu.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "A legnagyobb vizuális tartalomválasz azt az időpontot jelöli, amikor a rendszer megjeleníti a legnagyobb szöveget vagy képet. [További információ](https://web.dev/largest-contentful-paint)."
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Legnagyobb vizuális tartalomválasz"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "A felhasználók által tapasztalható, első interakciótól számított esetleges maximális válaszkésés a leghosszabb feladat időtartama. [További információ](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/id.json b/lighthouse-core/lib/i18n/locales/id.json
index 484ff4b43..27b34364c 100644
--- a/lighthouse-core/lib/i18n/locales/id.json
+++ b/lighthouse-core/lib/i18n/locales/id.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Largest Contentful Paint menandai waktu saat teks atau gambar terbesar di-paint. [Pelajari Lebih Lanjut](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Potensi maksimal Penundaan Input Pertama yang dapat dialami pengguna Anda adalah durasi tugas terpanjang. [Pelajari lebih lanjut](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/it.json b/lighthouse-core/lib/i18n/locales/it.json
index 27e4e8e65..1ff680385 100644
--- a/lighthouse-core/lib/i18n/locales/it.json
+++ b/lighthouse-core/lib/i18n/locales/it.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "La metrica Largest Contentful Paint (più grande visualizzazione con contenuti) indica il momento in cui vengono visualizzati il testo o l'immagine più grandi. [Ulteriori informazioni](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Il potenziale ritardo prima interazione massimo che i tuoi utenti potrebbero riscontrare è la durata del task più lungo. [Ulteriori informazioni](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ja.json b/lighthouse-core/lib/i18n/locales/ja.json
index e84efc64e..a592ca7ff 100644
--- a/lighthouse-core/lib/i18n/locales/ja.json
+++ b/lighthouse-core/lib/i18n/locales/ja.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "最大コンテンツの描画は、最も大きなテキストまたは画像が描画されるまでにかかった時間です。[詳細](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "最大コンテンツの描画"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "初回入力遅延の最大推定時間は、ユーザーが最も長いタスクを行うのにかかると推定される時間です。[詳細](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/ko.json b/lighthouse-core/lib/i18n/locales/ko.json
index 0f28f5688..56f840d67 100644
--- a/lighthouse-core/lib/i18n/locales/ko.json
+++ b/lighthouse-core/lib/i18n/locales/ko.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "콘텐츠가 포함된 최대 페인트는 최대 텍스트 또는 이미지가 표시되는 시간을 나타냅니다. [자세히 알아보기](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "콘텐츠가 포함된 최대 페인트"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "사용자가 경험할 수 있는 최대 첫 입력 지연 예상 시간은 가장 긴 작업의 길이입니다. [자세히 알아보기](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/lt.json b/lighthouse-core/lib/i18n/locales/lt.json
index 1cac4ab8b..258845e9c 100644
--- a/lighthouse-core/lib/i18n/locales/lt.json
+++ b/lighthouse-core/lib/i18n/locales/lt.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Didžiausias turiningas žymėjimas nurodo laiką, kada pažymimas didžiausias tekstas ar vaizdas. [Sužinokite daugiau](https://web.dev/largest-contentful-paint)."
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Didžiausias turiningas žymėjimas"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Didžiausia potenciali naudotojų patiriama pirmosios įvesties delsa yra ilgiausios užduoties trukmė. [Sužinokite daugiau](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/lv.json b/lighthouse-core/lib/i18n/locales/lv.json
index 696348cc0..72dead277 100644
--- a/lighthouse-core/lib/i18n/locales/lv.json
+++ b/lighthouse-core/lib/i18n/locales/lv.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Lielākais satura marķējums norāda laiku, kurā tiek atveidots apjomīgākais teksts vai attēls. [Uzziniet vairāk](https://web.dev/largest-contentful-paint)."
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Lielākais satura marķējums"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Iespējamā maksimālā pirmās ievades aizkave, ar ko jūsu lietotāji var saskarties, ir ilgākā uzdevuma ilgums. [Uzziniet vairāk](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/nl.json b/lighthouse-core/lib/i18n/locales/nl.json
index 0f1541bf1..d7b829233 100644
--- a/lighthouse-core/lib/i18n/locales/nl.json
+++ b/lighthouse-core/lib/i18n/locales/nl.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "'Grootste tekenbewerking met content' (LCP) geeft het tijdstip aan waarop de grootste tekst of afbeelding is weergegeven. [Meer informatie](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Grootste weergave met content (LCP)"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "De maximale potentiële vertraging voor de eerste invoer die gebruikers kunnen ervaren, is de duur van de langste taak. [Meer informatie](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/no.json b/lighthouse-core/lib/i18n/locales/no.json
index c0d1532eb..17a2cc5b7 100644
--- a/lighthouse-core/lib/i18n/locales/no.json
+++ b/lighthouse-core/lib/i18n/locales/no.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Største innholdsrike opptegning (LCP) markerer tidspunktet når den største teksten eller det største bildet vises. [Finn ut mer](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Største innholdsrike opptegning (LCP)"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Den maksimale potensielle forsinkelsen for første inndata som brukerne dine kan oppleve, er varigheten av den lengste oppgaven. [Finn ut mer](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/pl.json b/lighthouse-core/lib/i18n/locales/pl.json
index 51bedcd1e..2791634b9 100644
--- a/lighthouse-core/lib/i18n/locales/pl.json
+++ b/lighthouse-core/lib/i18n/locales/pl.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Największe wyrenderowanie treści oznacza czas wyrenderowania największego tekstu lub obrazu. [Więcej informacji](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Największe wyrenderowanie treści"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Maksymalne opóźnienie przy pierwszym działaniu, którego mogą doświadczyć użytkownicy, to czas wykonywania najdłuższego zadania. [Więcej informacji](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/pt-PT.json b/lighthouse-core/lib/i18n/locales/pt-PT.json
index bff89de0e..d3f66efee 100644
--- a/lighthouse-core/lib/i18n/locales/pt-PT.json
+++ b/lighthouse-core/lib/i18n/locales/pt-PT.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "A métrica Maior preenchimento com conteúdo assinala o momento de preenchimento com o maior texto ou a maior imagem. [Saber mais](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Maior preenchimento com conteúdo"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "O máximo potencial do primeiro atraso de entrada que pode afetar os utilizadores é a duração da tarefa mais longa. [Saber mais](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/pt.json b/lighthouse-core/lib/i18n/locales/pt.json
index eb19527d3..c0f22997e 100644
--- a/lighthouse-core/lib/i18n/locales/pt.json
+++ b/lighthouse-core/lib/i18n/locales/pt.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "\"Maior exibição de conteúdo\" marca o momento em que o maior texto ou imagem é exibido. [Saiba mais](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Maior exibição de conteúdo"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "A possível latência máxima na primeira entrada que seus usuários notariam seria a duração da tarefa mais longa. [Saiba mais](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ro.json b/lighthouse-core/lib/i18n/locales/ro.json
index 792c93914..1f8120bf9 100644
--- a/lighthouse-core/lib/i18n/locales/ro.json
+++ b/lighthouse-core/lib/i18n/locales/ro.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Cea mai mare redare de conținut semnalează momentul în care este redat cel mai mare text sau cea mai mare imagine. [Află mai multe](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Cea mai mare redare de conținut"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Timpul maxim potențial de la prima interacțiune cu care utilizatorii se pot confrunta este durata celei mai lungi activități. [Află mai multe](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ru.json b/lighthouse-core/lib/i18n/locales/ru.json
index c49d7328e..3246ecb53 100644
--- a/lighthouse-core/lib/i18n/locales/ru.json
+++ b/lighthouse-core/lib/i18n/locales/ru.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Отрисовка крупного контента – показатель, который определяет время, требуемое на полную отрисовку крупного текста или изображения. [Подробнее…](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Отрисовка крупного контента"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Максимальная потенциальная задержка после первого ввода показывает время выполнения самой длительной задачи. [Подробнее…](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/sk.json b/lighthouse-core/lib/i18n/locales/sk.json
index d10606d6c..b2d733208 100644
--- a/lighthouse-core/lib/i18n/locales/sk.json
+++ b/lighthouse-core/lib/i18n/locales/sk.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Najväčšie vykreslenie obsahu označuje čas, za ktorý sa vykreslí najväčší text alebo obrázok. [Ďalšie informácie](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Najväčšie vykreslenie obsahu"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Maximálne potenciálne oneskorenie prvého vstupu, ktoré sa u vašich používateľov môže vyskytnúť, je trvanie najdlhšej úlohy. [Ďalšie informácie](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/sl.json b/lighthouse-core/lib/i18n/locales/sl.json
index 366c14d10..7dbc63fed 100644
--- a/lighthouse-core/lib/i18n/locales/sl.json
+++ b/lighthouse-core/lib/i18n/locales/sl.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Največji vsebinski izris označuje čas, ko je izrisano največje besedilo oziroma je izrisana največja slika. [Več o tem](https://web.dev/largest-contentful-paint)."
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Največji vsebinski izris"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Največja potencialna zakasnitev od prvega vnosa, na katero lahko uporabniki naletijo, je trajanje najdaljšega opravila. [Več o tem](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/sr-Latn.json b/lighthouse-core/lib/i18n/locales/sr-Latn.json
index 32184bb3e..8d8770de9 100644
--- a/lighthouse-core/lib/i18n/locales/sr-Latn.json
+++ b/lighthouse-core/lib/i18n/locales/sr-Latn.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Najveće prikazivanje sadržaja označava trenutak u kojem se prikazuju najveći tekst ili slika. [Saznajte više](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Najveće prikazivanje sadržaja"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Maksimalno potencijalno kašnjenje prvog prikaza koje može da se desi korisnicima je trajanje najdužeg zadatka. [Saznajte više](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/sr.json b/lighthouse-core/lib/i18n/locales/sr.json
index 38228fee0..059f21a50 100644
--- a/lighthouse-core/lib/i18n/locales/sr.json
+++ b/lighthouse-core/lib/i18n/locales/sr.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Највеће приказивање садржаја означава тренутак у којем се приказују највећи текст или слика. [Сазнајте више](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Највеће приказивање садржаја"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Максимално потенцијално кашњење првог приказа које може да се деси корисницима је трајање најдужег задатка. [Сазнајте више](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/sv.json b/lighthouse-core/lib/i18n/locales/sv.json
index 15f2b1b2c..b7ed4e074 100644
--- a/lighthouse-core/lib/i18n/locales/sv.json
+++ b/lighthouse-core/lib/i18n/locales/sv.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Största uppritningen av innehåll anger tidpunkten då den största texten eller bilden ritades upp. [Läs mer](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Största uppritningen av innehåll"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Den högsta potentiella fördröjningen till första inmatningen som användarna kan få är längden på den längsta uppgiften. [Läs mer](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/ta.json b/lighthouse-core/lib/i18n/locales/ta.json
index 842b43413..d3adece2f 100644
--- a/lighthouse-core/lib/i18n/locales/ta.json
+++ b/lighthouse-core/lib/i18n/locales/ta.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn More](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "The maximum potential First Input Delay that your users could experience is the duration of the longest task. [Learn more](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/te.json b/lighthouse-core/lib/i18n/locales/te.json
index 81139bf17..2242ae8c1 100644
--- a/lighthouse-core/lib/i18n/locales/te.json
+++ b/lighthouse-core/lib/i18n/locales/te.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "అత్యంత పొడవైన కంటెంట్ గల పెయింట్ అనేది పొడవాటి వచనం లేదా చిత్రానికి పెయింట్ వేసిన సమయాన్ని గుర్తిస్తుంది. [మరింత తెలుసుకోండి](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "కంటెంట్ కలిగి ఉండే అతిపెద్ద పెయింట్"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "మీ యూజర్‌లు ఎదుర్కోగల గరిష్ఠ మొదటి ఇన్‌పుట్ ఆలస్యం అన్నది సుదీర్ఘ టాస్క్ సమయ వ్యవధిని కలిగి ఉంటుంది. [మరింత తెలుసుకోండి](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/th.json b/lighthouse-core/lib/i18n/locales/th.json
index e355c31fb..41402fb56 100644
--- a/lighthouse-core/lib/i18n/locales/th.json
+++ b/lighthouse-core/lib/i18n/locales/th.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Largest Contentful Paint ระบุเวลาที่แสดงผลข้อความหรือรูปภาพได้มากที่สุด [ดูข้อมูลเพิ่มเติม](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Largest Contentful Paint"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "First Input Delay สูงสุดที่อาจเกิดขึ้นซึ่งผู้ใช้อาจเจอคือระยะเวลาของงานที่ยาวที่สุด [ดูข้อมูลเพิ่มเติม](https://web.dev/lighthouse-max-potential-fid)"
   },
diff --git a/lighthouse-core/lib/i18n/locales/tr.json b/lighthouse-core/lib/i18n/locales/tr.json
index 10dabe505..2f0200ca1 100644
--- a/lighthouse-core/lib/i18n/locales/tr.json
+++ b/lighthouse-core/lib/i18n/locales/tr.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "En Büyük Zengin İçerikli Boyama, en büyük metnin veya resmin boyandığı zamanı işaret eder. [Daha Fazla Bilgi](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "En Büyük Zengin İçerikli Boya"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Kullanıcılarınızın karşılaşabileceği olası maksimum İlk Giriş Gecikmesi, en uzun görevin süresidir. [Daha fazla bilgi](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/uk.json b/lighthouse-core/lib/i18n/locales/uk.json
index acd40892a..fc4d00b4d 100644
--- a/lighthouse-core/lib/i18n/locales/uk.json
+++ b/lighthouse-core/lib/i18n/locales/uk.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Найбільша візуалізація контенту показує, коли з'являється найбільший текст чи зображення. [Докладніше](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Найбільша візуалізація контенту"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Максимальна потенційна затримка відповіді на першу дію – це тривалість найдовшого завдання. [Докладніше](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/vi.json b/lighthouse-core/lib/i18n/locales/vi.json
index 0c6355864..d05ccee9e 100644
--- a/lighthouse-core/lib/i18n/locales/vi.json
+++ b/lighthouse-core/lib/i18n/locales/vi.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "Chỉ số Hiển thị nội dung có kích thước lớn nhất đánh dấu thời điểm hiển thị văn bản hoặc hình ảnh có kích thước lớn nhất. [Tìm hiểu thêm](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "Chỉ số Hiển thị nội dung có kích thước lớn nhất"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "Thời gian phản hồi lần tương tác đầu tiên tối đa mà người dùng có thể gặp phải là thời gian thực hiện nhiệm vụ lâu nhất. [Tìm hiểu thêm](https://web.dev/lighthouse-max-potential-fid)."
   },
diff --git a/lighthouse-core/lib/i18n/locales/zh-HK.json b/lighthouse-core/lib/i18n/locales/zh-HK.json
index bcb3aeac7..b4f5ca56e 100644
--- a/lighthouse-core/lib/i18n/locales/zh-HK.json
+++ b/lighthouse-core/lib/i18n/locales/zh-HK.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "「最大內容繪製」是指繪製最大的文字或圖片的時間。[瞭解詳情](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "最大內容繪製"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "使用者可以體驗到最長的「首次輸入延遲時間」就是最長的工作持續時間。[瞭解詳情](https://web.dev/lighthouse-max-potential-fid)。"
   },
diff --git a/lighthouse-core/lib/i18n/locales/zh-TW.json b/lighthouse-core/lib/i18n/locales/zh-TW.json
index cafbb45ae..b59e60ebc 100644
--- a/lighthouse-core/lib/i18n/locales/zh-TW.json
+++ b/lighthouse-core/lib/i18n/locales/zh-TW.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "「最大內容繪製」是指繪製最大的文字或圖片所需時間。[瞭解詳情](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "最大內容繪製"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "使用者可能會遇到的最長「首次輸入延遲時間」就是耗時最久的工作持續時間。[瞭解詳情](https://web.dev/lighthouse-max-potential-fid)。"
   },
diff --git a/lighthouse-core/lib/i18n/locales/zh.json b/lighthouse-core/lib/i18n/locales/zh.json
index 4061e023d..5b0b0695f 100644
--- a/lighthouse-core/lib/i18n/locales/zh.json
+++ b/lighthouse-core/lib/i18n/locales/zh.json
@@ -842,9 +842,6 @@
   "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": {
     "message": "最大内容渲染时间标记了渲染出最大文本或图片的时间。[了解详情](https://web.dev/largest-contentful-paint)"
   },
-  "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": {
-    "message": "最大内容渲染"
-  },
   "lighthouse-core/audits/metrics/max-potential-fid.js | description": {
     "message": "您的用户可能会遇到的最长首次输入延迟是用时最长的任务的耗时。[了解详情](https://web.dev/lighthouse-max-potential-fid)。"
   },
diff --git a/lighthouse-core/scripts/legacy-javascript/README.md b/lighthouse-core/scripts/legacy-javascript/README.md
index 74cd54181..27a0eb614 100644
--- a/lighthouse-core/scripts/legacy-javascript/README.md
+++ b/lighthouse-core/scripts/legacy-javascript/README.md
@@ -5,7 +5,7 @@ Creates many projects using specific babel transforms / polyfills (called varian
 Run:
 
 ```sh
-yarn install
+yarn
 node run.js
 # STAGE=build|audit|all to just build the audits or run LegacyJavascript on them. Defaults to both (`all`).
 ```
diff --git a/lighthouse-core/scripts/legacy-javascript/run.js b/lighthouse-core/scripts/legacy-javascript/run.js
index 591e56cb3..d29c03abb 100644
--- a/lighthouse-core/scripts/legacy-javascript/run.js
+++ b/lighthouse-core/scripts/legacy-javascript/run.js
@@ -34,122 +34,7 @@ const STAGE = process.env.STAGE || 'all';
 const mainCode = fs.readFileSync(`${__dirname}/main.js`, 'utf-8');
 
 const plugins = LegacyJavascript.getTransformPatterns().map(pattern => pattern.name);
-
-// TODO(cjamcl): Get this from `LegacyJavascript`. Data isn't structured ideally yet, but should be
-// done when SourceMap support is in.
-const polyfills = [
-  'es6.array.copy-within',
-  'es6.array.every',
-  'es6.array.fill',
-  'es6.array.filter',
-  'es6.array.find-index',
-  'es6.array.find',
-  'es6.array.for-each',
-  'es6.array.from',
-  'es6.array.index-of',
-  'es6.array.is-array',
-  'es6.array.iterator',
-  'es6.array.last-index-of',
-  'es6.array.map',
-  'es6.array.of',
-  'es6.array.reduce-right',
-  'es6.array.reduce',
-  'es6.array.some',
-  'es6.array.species',
-  'es6.date.now',
-  'es6.date.to-iso-string',
-  'es6.date.to-json',
-  'es6.date.to-primitive',
-  'es6.date.to-string',
-  'es6.function.bind',
-  'es6.function.has-instance',
-  'es6.function.name',
-  'es6.map',
-  'es6.math.acosh',
-  'es6.math.asinh',
-  'es6.math.atanh',
-  'es6.math.cbrt',
-  'es6.math.clz32',
-  'es6.math.cosh',
-  'es6.math.expm1',
-  'es6.math.fround',
-  'es6.math.hypot',
-  'es6.math.imul',
-  'es6.math.log10',
-  'es6.math.log1p',
-  'es6.math.log2',
-  'es6.math.sign',
-  'es6.math.sinh',
-  'es6.math.tanh',
-  'es6.math.trunc',
-  'es6.number.constructor',
-  'es6.number.epsilon',
-  'es6.number.is-integer',
-  'es6.number.is-safe-integer',
-  'es6.number.max-safe-integer',
-  'es6.number.min-safe-integer',
-  'es6.number.parse-float',
-  'es6.number.parse-int',
-  'es6.object.assign',
-  'es6.object.create',
-  'es6.object.define-properties',
-  'es6.object.define-property',
-  'es6.object.freeze',
-  'es6.object.get-own-property-descriptor',
-  'es6.object.get-own-property-names',
-  'es6.object.get-prototype-of',
-  'es6.object.is-extensible',
-  'es6.object.is-frozen',
-  'es6.object.is-sealed',
-  'es6.object.keys',
-  'es6.object.prevent-extensions',
-  'es6.object.seal',
-  'es6.object.set-prototype-of',
-  'es6.object.to-string',
-  'es6.promise',
-  'es6.reflect.apply',
-  'es6.reflect.construct',
-  'es6.reflect.define-property',
-  'es6.reflect.delete-property',
-  'es6.reflect.get-own-property-descriptor',
-  'es6.reflect.get-prototype-of',
-  'es6.reflect.get',
-  'es6.reflect.has',
-  'es6.reflect.is-extensible',
-  'es6.reflect.own-keys',
-  'es6.reflect.prevent-extensions',
-  'es6.reflect.set-prototype-of',
-  'es6.reflect.set',
-  'es6.set',
-  'es6.string.code-point-at',
-  'es6.string.ends-with',
-  'es6.string.from-code-point',
-  'es6.string.includes',
-  'es6.string.iterator',
-  'es6.string.raw',
-  'es6.string.repeat',
-  'es6.string.starts-with',
-  'es6.string.trim',
-  'es6.typed.array-buffer',
-  'es6.typed.data-view',
-  'es6.typed.float32-array',
-  'es6.typed.float64-array',
-  'es6.typed.int16-array',
-  'es6.typed.int32-array',
-  'es6.typed.int8-array',
-  'es6.typed.uint16-array',
-  'es6.typed.uint32-array',
-  'es6.typed.uint8-array',
-  'es6.typed.uint8-clamped-array',
-  'es6.weak-map',
-  'es6.weak-set',
-  'es7.array.includes',
-  'es7.object.entries',
-  'es7.object.get-own-property-descriptors',
-  'es7.object.values',
-  'es7.string.pad-end',
-  'es7.string.pad-start',
-];
+const polyfills = LegacyJavascript.getPolyfillData().map(d => d.module);
 
 /**
  * @param {string} command
@@ -191,7 +76,8 @@ async function createVariant(options) {
     fs.writeFileSync(`${dir}/.babelrc`, JSON.stringify(babelrc || {}, null, 2));
     // Not used in this script, but useful for running Lighthouse manually.
     // Just need to start a web server first.
-    fs.writeFileSync(`${dir}/index.html`, `<title>${name}</title><script src=main.bundle.min.js>`);
+    fs.writeFileSync(`${dir}/index.html`,
+      `<title>${name}</title><script src=main.bundle.min.js></script><p>${name}</p>`);
 
     // Note: No babelrc will make babel a glorified `cp`.
     runCommand('yarn', [
@@ -200,6 +86,7 @@ async function createVariant(options) {
       '--config-file', `${dir}/.babelrc`,
       '--ignore', 'node_modules/**/*.js',
       '-o', `${dir}/main.transpiled.js`,
+      '--source-maps', 'inline',
     ]);
 
     // Transform any require statements (like for core-js) into a big bundle.
@@ -207,6 +94,7 @@ async function createVariant(options) {
       'browserify',
       `${dir}/main.transpiled.js`,
       '-o', `${dir}/main.bundle.js`,
+      '--debug', // source maps
     ]);
 
     // Minify.
@@ -214,51 +102,78 @@ async function createVariant(options) {
       'terser',
       `${dir}/main.bundle.js`,
       '-o', `${dir}/main.bundle.min.js`,
+      '--source-map', 'content="inline",url="main.bundle.min.js.map"',
     ]);
   }
 
   if (STAGE === 'audit' || STAGE === 'all') {
-    // Instead of running Lighthouse, use LegacyJavascript directly. Requires some setup.
-    // Much faster than running Lighthouse.
-    const documentUrl = 'http://localhost/index.html'; // These URLs don't matter.
-    const scriptUrl = 'https://localhost/main.bundle.min.js';
-    const networkRecords = [
-      {url: documentUrl},
-      {url: scriptUrl},
-    ];
-    const devtoolsLogs = networkRecordsToDevtoolsLog(networkRecords);
-    const jsRequestWillBeSentEvent = devtoolsLogs.find(e =>
-      e.method === 'Network.requestWillBeSent' && e.params.request.url === scriptUrl);
-    if (!jsRequestWillBeSentEvent) throw new Error('jsRequestWillBeSentEvent is undefined');
-    // @ts-ignore - the log event is not narrowed to 'Network.requestWillBeSent' event from find
-    const jsRequestId = jsRequestWillBeSentEvent.params.requestId;
-    const code = fs.readFileSync(`${dir}/main.bundle.min.js`, 'utf-8').toString();
-    /** @type {Pick<LH.Artifacts, 'devtoolsLogs'|'URL'|'ScriptElements'>} */
-    const artifacts = {
-      URL: {finalUrl: documentUrl, requestedUrl: documentUrl},
-      devtoolsLogs: {
-        [LegacyJavascript.DEFAULT_PASS]: devtoolsLogs,
-      },
-      ScriptElements: [
-        // @ts-ignore - partial ScriptElement excluding unused DOM properties
-        {requestId: jsRequestId, content: code},
-      ],
-    };
-    // @ts-ignore: partial Artifacts.
-    const legacyJavascriptResults = await LegacyJavascript.audit(artifacts, {
-      computedCache: new Map(),
-    });
+    const code = fs.readFileSync(`${dir}/main.bundle.min.js`, 'utf-8');
+    const map = JSON.parse(fs.readFileSync(`${dir}/main.bundle.min.js.map`, 'utf-8'));
+
+    let legacyJavascriptResults;
+
+    legacyJavascriptResults = await getLegacyJavascriptResults(code, map, {sourceMaps: true});
     fs.writeFileSync(`${dir}/legacy-javascript.json`,
+      // @ts-ignore: Items will exist.
+      JSON.stringify(legacyJavascriptResults.details.items, null, 2));
+
+    legacyJavascriptResults = await getLegacyJavascriptResults(code, map, {sourceMaps: false});
+    fs.writeFileSync(`${dir}/legacy-javascript-nomaps.json`,
+      // @ts-ignore: Items will exist.
       JSON.stringify(legacyJavascriptResults.details.items, null, 2));
   }
 }
 
-function makeSummary() {
+/**
+ * @param {string} code
+ * @param {LH.Artifacts.RawSourceMap} map
+ * @param {{sourceMaps: boolean}} _
+ * @return {Promise<LH.Audit.Product>}
+ */
+function getLegacyJavascriptResults(code, map, {sourceMaps}) {
+  // Instead of running Lighthouse, use LegacyJavascript directly. Requires some setup.
+  // Much faster than running Lighthouse.
+  const documentUrl = 'http://localhost/index.html'; // These URLs don't matter.
+  const scriptUrl = 'https://localhost/main.bundle.min.js';
+  const networkRecords = [
+    {url: documentUrl},
+    {url: scriptUrl},
+  ];
+  const devtoolsLogs = networkRecordsToDevtoolsLog(networkRecords);
+  const jsRequestWillBeSentEvent = devtoolsLogs.find(e =>
+    e.method === 'Network.requestWillBeSent' && e.params.request.url === scriptUrl);
+  if (!jsRequestWillBeSentEvent) throw new Error('jsRequestWillBeSentEvent is undefined');
+  // @ts-ignore - the log event is not narrowed to 'Network.requestWillBeSent' event from find
+  const jsRequestId = jsRequestWillBeSentEvent.params.requestId;
+
+  /** @type {Pick<LH.Artifacts, 'devtoolsLogs'|'URL'|'ScriptElements'|'SourceMaps'>} */
+  const artifacts = {
+    URL: {finalUrl: documentUrl, requestedUrl: documentUrl},
+    devtoolsLogs: {
+      [LegacyJavascript.DEFAULT_PASS]: devtoolsLogs,
+    },
+    ScriptElements: [
+      // @ts-ignore - partial ScriptElement excluding unused DOM properties
+      {src: scriptUrl, requestId: jsRequestId, content: code},
+    ],
+    SourceMaps: [],
+  };
+  if (sourceMaps) artifacts.SourceMaps = [{scriptUrl, map}];
+  // @ts-ignore: partial Artifacts.
+  return LegacyJavascript.audit(artifacts, {
+    computedCache: new Map(),
+  });
+}
+
+/**
+ * @param {string} legacyJavascriptFilename
+ */
+function makeSummary(legacyJavascriptFilename) {
   let totalSignals = 0;
   const variants = [];
   for (const dir of glob.sync('*/*', {cwd: VARIANT_DIR})) {
     /** @type {Array<{signals: string[]}>} */
-    const legacyJavascriptItems = require(`${VARIANT_DIR}/${dir}/legacy-javascript.json`);
+    const legacyJavascriptItems = require(`${VARIANT_DIR}/${dir}/${legacyJavascriptFilename}`);
     const signals = legacyJavascriptItems.reduce((acc, cur) => {
       totalSignals += cur.signals.length;
       return acc.concat(cur.signals);
@@ -349,7 +264,10 @@ async function main() {
 
   removeCoreJs();
 
-  const summary = makeSummary();
+  let summary;
+
+  // Summary of using source maps and pattern matching.
+  summary = makeSummary('legacy-javascript.json');
   fs.writeFileSync(`${__dirname}/summary-signals.json`, JSON.stringify(summary, null, 2));
   console.log({
     totalSignals: summary.totalSignals,
@@ -357,6 +275,10 @@ async function main() {
   });
   console.table(summary.variants);
 
+  // Summary of using only pattern matching.
+  summary = makeSummary('legacy-javascript-nomaps.json');
+  fs.writeFileSync(`${__dirname}/summary-signals-nomaps.json`, JSON.stringify(summary, null, 2));
+
   createSummarySizes();
 }
 
diff --git a/lighthouse-core/scripts/legacy-javascript/summary-signals-nomaps.json b/lighthouse-core/scripts/legacy-javascript/summary-signals-nomaps.json
new file mode 100644
index 000000000..80c99eb6b
--- /dev/null
+++ b/lighthouse-core/scripts/legacy-javascript/summary-signals-nomaps.json
@@ -0,0 +1,715 @@
+{
+  "totalSignals": 205,
+  "variantsMissingSignals": [
+    "core-js-2-only-polyfill/es6-array-filter",
+    "core-js-2-only-polyfill/es6-array-map",
+    "core-js-2-only-polyfill/es6-array-reduce",
+    "core-js-2-only-polyfill/es6-array-reduce-right",
+    "core-js-2-only-polyfill/es6-array-some",
+    "core-js-2-only-polyfill/es6-date-to-string",
+    "core-js-2-only-polyfill/es6-function-name",
+    "core-js-2-only-polyfill/es6-object-freeze",
+    "core-js-2-only-polyfill/es6-object-get-own-property-descriptor",
+    "core-js-2-only-polyfill/es6-object-get-own-property-names",
+    "core-js-2-only-polyfill/es6-object-get-prototype-of",
+    "core-js-2-only-polyfill/es6-object-is-extensible",
+    "core-js-2-only-polyfill/es6-object-is-frozen",
+    "core-js-2-only-polyfill/es6-object-is-sealed",
+    "core-js-2-only-polyfill/es6-object-keys",
+    "core-js-2-only-polyfill/es6-object-prevent-extensions",
+    "core-js-2-only-polyfill/es6-object-seal",
+    "core-js-2-only-polyfill/es6-reflect-define-property",
+    "core-js-2-only-polyfill/es6-string-trim",
+    "core-js-2-preset-env-esmodules/true",
+    "core-js-3-only-polyfill/es6-array-filter",
+    "core-js-3-only-polyfill/es6-array-map",
+    "core-js-3-only-polyfill/es6-array-reduce",
+    "core-js-3-only-polyfill/es6-array-reduce-right",
+    "core-js-3-only-polyfill/es6-array-some",
+    "core-js-3-only-polyfill/es6-date-to-string",
+    "core-js-3-only-polyfill/es6-function-name",
+    "core-js-3-only-polyfill/es6-object-freeze",
+    "core-js-3-only-polyfill/es6-object-get-own-property-descriptor",
+    "core-js-3-only-polyfill/es6-object-get-own-property-names",
+    "core-js-3-only-polyfill/es6-object-get-prototype-of",
+    "core-js-3-only-polyfill/es6-object-is-extensible",
+    "core-js-3-only-polyfill/es6-object-is-frozen",
+    "core-js-3-only-polyfill/es6-object-is-sealed",
+    "core-js-3-only-polyfill/es6-object-keys",
+    "core-js-3-only-polyfill/es6-object-prevent-extensions",
+    "core-js-3-only-polyfill/es6-object-seal",
+    "core-js-3-only-polyfill/es6-reflect-define-property",
+    "core-js-3-only-polyfill/es6-string-trim",
+    "core-js-3-preset-env-esmodules/true"
+  ],
+  "variants": [
+    {
+      "name": "core-js-2-only-polyfill/es6-array-fill",
+      "signals": "Array.prototype.fill"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-filter",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-find",
+      "signals": "Array.prototype.find"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-find-index",
+      "signals": "Array.prototype.findIndex"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-for-each",
+      "signals": "Array.prototype.forEach"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-from",
+      "signals": "Array.from"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-is-array",
+      "signals": "Array.isArray"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-last-index-of",
+      "signals": "Array.prototype.lastIndexOf"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-map",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-of",
+      "signals": "Array.of"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-reduce",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-reduce-right",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-array-some",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-date-now",
+      "signals": "Date.now"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-date-to-iso-string",
+      "signals": "Date.prototype.toISOString"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-date-to-json",
+      "signals": "Date.prototype.toJSON"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-date-to-string",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-function-name",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-map",
+      "signals": "Map"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-number-is-integer",
+      "signals": "Number.isInteger"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-number-is-safe-integer",
+      "signals": "Number.isSafeInteger"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-number-parse-float",
+      "signals": "Number.parseFloat"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-number-parse-int",
+      "signals": "Number.parseInt"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-assign",
+      "signals": "Object.assign"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-create",
+      "signals": "Object.create"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-define-properties",
+      "signals": "Object.defineProperties"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-define-property",
+      "signals": "Object.defineProperty"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-freeze",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-get-own-property-descriptor",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-get-own-property-names",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-get-prototype-of",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-is-extensible",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-is-frozen",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-is-sealed",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-keys",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-prevent-extensions",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-seal",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-object-set-prototype-of",
+      "signals": "Object.setPrototypeOf"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-promise",
+      "signals": "Promise"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-apply",
+      "signals": "Reflect.apply"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-construct",
+      "signals": "Reflect.construct"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-define-property",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-delete-property",
+      "signals": "Reflect.deleteProperty"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-get",
+      "signals": "Reflect.get"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-get-own-property-descriptor",
+      "signals": "Reflect.getOwnPropertyDescriptor"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-get-prototype-of",
+      "signals": "Reflect.getPrototypeOf"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-has",
+      "signals": "Reflect.has"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-is-extensible",
+      "signals": "Reflect.isExtensible"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-own-keys",
+      "signals": "Reflect.ownKeys"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-prevent-extensions",
+      "signals": "Reflect.preventExtensions"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-set",
+      "signals": "Reflect.set"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-reflect-set-prototype-of",
+      "signals": "Reflect.setPrototypeOf"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-set",
+      "signals": "Set"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-string-code-point-at",
+      "signals": "String.prototype.codePointAt"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-string-ends-with",
+      "signals": "String.prototype.endsWith"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-string-from-code-point",
+      "signals": "String.fromCodePoint"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-string-includes",
+      "signals": "String.prototype.includes"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-string-raw",
+      "signals": "String.raw"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-string-repeat",
+      "signals": "String.prototype.repeat"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-string-starts-with",
+      "signals": "String.prototype.startsWith"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-string-trim",
+      "signals": ""
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-array-buffer",
+      "signals": "ArrayBuffer, DataView"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-data-view",
+      "signals": "ArrayBuffer, DataView"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-float32-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Float32Array"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-float64-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Float64Array"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-int16-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Int16Array"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-int32-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Int32Array"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-int8-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Int8Array"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-uint16-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Uint16Array"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-uint32-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Uint32Array"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-uint8-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-typed-uint8-clamped-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Uint8ClampedArray"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-weak-map",
+      "signals": "WeakMap"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es6-weak-set",
+      "signals": "WeakSet"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es7-array-includes",
+      "signals": "Array.prototype.includes"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es7-object-entries",
+      "signals": "Object.entries"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es7-object-get-own-property-descriptors",
+      "signals": "Object.getOwnPropertyDescriptors"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es7-object-values",
+      "signals": "Object.values"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es7-string-pad-end",
+      "signals": "String.prototype.padEnd"
+    },
+    {
+      "name": "core-js-2-only-polyfill/es7-string-pad-start",
+      "signals": "String.prototype.padStart"
+    },
+    {
+      "name": "core-js-2-preset-env-esmodules/false",
+      "signals": "@babel/plugin-transform-classes, @babel/plugin-transform-regenerator, @babel/plugin-transform-spread"
+    },
+    {
+      "name": "core-js-2-preset-env-esmodules/true",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-fill",
+      "signals": "Array.prototype.fill"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-filter",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-find",
+      "signals": "Array.prototype.find"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-find-index",
+      "signals": "Array.prototype.findIndex"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-for-each",
+      "signals": "Array.prototype.forEach"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-from",
+      "signals": "Array.from"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-is-array",
+      "signals": "Array.isArray"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-last-index-of",
+      "signals": "Array.prototype.lastIndexOf"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-map",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-of",
+      "signals": "Array.of"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-reduce",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-reduce-right",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-array-some",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-date-now",
+      "signals": "Date.now"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-date-to-iso-string",
+      "signals": "Date.prototype.toISOString"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-date-to-json",
+      "signals": "Date.prototype.toJSON"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-date-to-string",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-function-name",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-map",
+      "signals": "Map"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-number-is-integer",
+      "signals": "Number.isInteger"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-number-is-safe-integer",
+      "signals": "Number.isSafeInteger"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-number-parse-float",
+      "signals": "Number.parseFloat"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-number-parse-int",
+      "signals": "Number.parseInt"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-assign",
+      "signals": "Object.assign"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-create",
+      "signals": "Object.create"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-define-properties",
+      "signals": "Object.defineProperties"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-define-property",
+      "signals": "Object.defineProperty"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-freeze",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-get-own-property-descriptor",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-get-own-property-names",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-get-prototype-of",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-is-extensible",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-is-frozen",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-is-sealed",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-keys",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-prevent-extensions",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-seal",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-object-set-prototype-of",
+      "signals": "Object.setPrototypeOf"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-promise",
+      "signals": "Promise"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-apply",
+      "signals": "Reflect.apply"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-construct",
+      "signals": "Reflect.construct"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-define-property",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-delete-property",
+      "signals": "Reflect.deleteProperty"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-get",
+      "signals": "Reflect.get"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-get-own-property-descriptor",
+      "signals": "Reflect.getOwnPropertyDescriptor"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-get-prototype-of",
+      "signals": "Reflect.getPrototypeOf"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-has",
+      "signals": "Reflect.has"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-is-extensible",
+      "signals": "Reflect.isExtensible"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-own-keys",
+      "signals": "Reflect.ownKeys"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-prevent-extensions",
+      "signals": "Reflect.preventExtensions"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-set",
+      "signals": "Reflect.set"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-reflect-set-prototype-of",
+      "signals": "Reflect.setPrototypeOf"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-set",
+      "signals": "Set"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-string-code-point-at",
+      "signals": "String.prototype.codePointAt"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-string-ends-with",
+      "signals": "String.prototype.endsWith"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-string-from-code-point",
+      "signals": "String.fromCodePoint"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-string-includes",
+      "signals": "String.prototype.includes"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-string-raw",
+      "signals": "String.raw"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-string-repeat",
+      "signals": "String.prototype.repeat"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-string-starts-with",
+      "signals": "String.prototype.startsWith"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-string-trim",
+      "signals": ""
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-array-buffer",
+      "signals": "ArrayBuffer, DataView"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-data-view",
+      "signals": "ArrayBuffer, DataView"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-float32-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Float32Array"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-float64-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Float64Array"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-int16-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Int16Array"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-int32-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Int32Array"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-int8-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Int8Array"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-uint16-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Uint16Array"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-uint32-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Uint32Array"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-uint8-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-typed-uint8-clamped-array",
+      "signals": "Uint8Array, Object.getOwnPropertyDescriptor, ArrayBuffer, DataView, Uint8ClampedArray"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-weak-map",
+      "signals": "WeakMap"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es6-weak-set",
+      "signals": "WeakSet"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es7-array-includes",
+      "signals": "Array.prototype.includes"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es7-object-entries",
+      "signals": "Object.entries"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es7-object-get-own-property-descriptors",
+      "signals": "Object.getOwnPropertyDescriptors"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es7-object-values",
+      "signals": "Object.values"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es7-string-pad-end",
+      "signals": "String.prototype.padEnd"
+    },
+    {
+      "name": "core-js-3-only-polyfill/es7-string-pad-start",
+      "signals": "String.prototype.padStart"
+    },
+    {
+      "name": "core-js-3-preset-env-esmodules/false",
+      "signals": "@babel/plugin-transform-classes, @babel/plugin-transform-regenerator, @babel/plugin-transform-spread"
+    },
+    {
+      "name": "core-js-3-preset-env-esmodules/true",
+      "signals": ""
+    },
+    {
+      "name": "only-plugin/-babel-plugin-transform-classes",
+      "signals": "@babel/plugin-transform-classes"
+    },
+    {
+      "name": "only-plugin/-babel-plugin-transform-regenerator",
+      "signals": "@babel/plugin-transform-regenerator"
+    },
+    {
+      "name": "only-plugin/-babel-plugin-transform-spread",
+      "signals": "@babel/plugin-transform-spread"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/lighthouse-core/scripts/legacy-javascript/summary-signals.json b/lighthouse-core/scripts/legacy-javascript/summary-signals.json
index e5967863f..af3a7b838 100644
--- a/lighthouse-core/scripts/legacy-javascript/summary-signals.json
+++ b/lighthouse-core/scripts/legacy-javascript/summary-signals.json
@@ -1,125 +1,17 @@
 {
-  "totalSignals": 205,
+  "totalSignals": 243,
   "variantsMissingSignals": [
-    "core-js-2-only-polyfill/es6-array-copy-within",
-    "core-js-2-only-polyfill/es6-array-every",
-    "core-js-2-only-polyfill/es6-array-filter",
-    "core-js-2-only-polyfill/es6-array-index-of",
-    "core-js-2-only-polyfill/es6-array-iterator",
-    "core-js-2-only-polyfill/es6-array-map",
-    "core-js-2-only-polyfill/es6-array-reduce",
-    "core-js-2-only-polyfill/es6-array-reduce-right",
-    "core-js-2-only-polyfill/es6-array-some",
-    "core-js-2-only-polyfill/es6-array-species",
-    "core-js-2-only-polyfill/es6-date-to-primitive",
-    "core-js-2-only-polyfill/es6-date-to-string",
-    "core-js-2-only-polyfill/es6-function-bind",
-    "core-js-2-only-polyfill/es6-function-has-instance",
-    "core-js-2-only-polyfill/es6-function-name",
-    "core-js-2-only-polyfill/es6-math-acosh",
-    "core-js-2-only-polyfill/es6-math-asinh",
-    "core-js-2-only-polyfill/es6-math-atanh",
-    "core-js-2-only-polyfill/es6-math-cbrt",
-    "core-js-2-only-polyfill/es6-math-clz32",
-    "core-js-2-only-polyfill/es6-math-cosh",
-    "core-js-2-only-polyfill/es6-math-expm1",
-    "core-js-2-only-polyfill/es6-math-fround",
-    "core-js-2-only-polyfill/es6-math-hypot",
-    "core-js-2-only-polyfill/es6-math-imul",
-    "core-js-2-only-polyfill/es6-math-log10",
-    "core-js-2-only-polyfill/es6-math-log1p",
-    "core-js-2-only-polyfill/es6-math-log2",
-    "core-js-2-only-polyfill/es6-math-sign",
-    "core-js-2-only-polyfill/es6-math-sinh",
-    "core-js-2-only-polyfill/es6-math-tanh",
-    "core-js-2-only-polyfill/es6-math-trunc",
-    "core-js-2-only-polyfill/es6-number-constructor",
-    "core-js-2-only-polyfill/es6-number-epsilon",
-    "core-js-2-only-polyfill/es6-number-max-safe-integer",
-    "core-js-2-only-polyfill/es6-number-min-safe-integer",
-    "core-js-2-only-polyfill/es6-object-freeze",
-    "core-js-2-only-polyfill/es6-object-get-own-property-descriptor",
-    "core-js-2-only-polyfill/es6-object-get-own-property-names",
-    "core-js-2-only-polyfill/es6-object-get-prototype-of",
-    "core-js-2-only-polyfill/es6-object-is-extensible",
-    "core-js-2-only-polyfill/es6-object-is-frozen",
-    "core-js-2-only-polyfill/es6-object-is-sealed",
-    "core-js-2-only-polyfill/es6-object-keys",
-    "core-js-2-only-polyfill/es6-object-prevent-extensions",
-    "core-js-2-only-polyfill/es6-object-seal",
-    "core-js-2-only-polyfill/es6-object-to-string",
-    "core-js-2-only-polyfill/es6-reflect-define-property",
-    "core-js-2-only-polyfill/es6-string-iterator",
-    "core-js-2-only-polyfill/es6-string-trim",
     "core-js-2-preset-env-esmodules/true",
-    "core-js-3-only-polyfill/es6-array-copy-within",
-    "core-js-3-only-polyfill/es6-array-every",
-    "core-js-3-only-polyfill/es6-array-filter",
-    "core-js-3-only-polyfill/es6-array-index-of",
-    "core-js-3-only-polyfill/es6-array-iterator",
-    "core-js-3-only-polyfill/es6-array-map",
-    "core-js-3-only-polyfill/es6-array-reduce",
-    "core-js-3-only-polyfill/es6-array-reduce-right",
-    "core-js-3-only-polyfill/es6-array-some",
-    "core-js-3-only-polyfill/es6-array-species",
-    "core-js-3-only-polyfill/es6-date-to-primitive",
-    "core-js-3-only-polyfill/es6-date-to-string",
-    "core-js-3-only-polyfill/es6-function-bind",
-    "core-js-3-only-polyfill/es6-function-has-instance",
-    "core-js-3-only-polyfill/es6-function-name",
-    "core-js-3-only-polyfill/es6-math-acosh",
-    "core-js-3-only-polyfill/es6-math-asinh",
-    "core-js-3-only-polyfill/es6-math-atanh",
-    "core-js-3-only-polyfill/es6-math-cbrt",
-    "core-js-3-only-polyfill/es6-math-clz32",
-    "core-js-3-only-polyfill/es6-math-cosh",
-    "core-js-3-only-polyfill/es6-math-expm1",
-    "core-js-3-only-polyfill/es6-math-fround",
-    "core-js-3-only-polyfill/es6-math-hypot",
-    "core-js-3-only-polyfill/es6-math-imul",
-    "core-js-3-only-polyfill/es6-math-log10",
-    "core-js-3-only-polyfill/es6-math-log1p",
-    "core-js-3-only-polyfill/es6-math-log2",
-    "core-js-3-only-polyfill/es6-math-sign",
-    "core-js-3-only-polyfill/es6-math-sinh",
-    "core-js-3-only-polyfill/es6-math-tanh",
-    "core-js-3-only-polyfill/es6-math-trunc",
-    "core-js-3-only-polyfill/es6-number-constructor",
-    "core-js-3-only-polyfill/es6-number-epsilon",
-    "core-js-3-only-polyfill/es6-number-max-safe-integer",
-    "core-js-3-only-polyfill/es6-number-min-safe-integer",
-    "core-js-3-only-polyfill/es6-object-freeze",
-    "core-js-3-only-polyfill/es6-object-get-own-property-descriptor",
-    "core-js-3-only-polyfill/es6-object-get-own-property-names",
-    "core-js-3-only-polyfill/es6-object-get-prototype-of",
-    "core-js-3-only-polyfill/es6-object-is-extensible",
-    "core-js-3-only-polyfill/es6-object-is-frozen",
-    "core-js-3-only-polyfill/es6-object-is-sealed",
-    "core-js-3-only-polyfill/es6-object-keys",
-    "core-js-3-only-polyfill/es6-object-prevent-extensions",
-    "core-js-3-only-polyfill/es6-object-seal",
-    "core-js-3-only-polyfill/es6-object-to-string",
-    "core-js-3-only-polyfill/es6-reflect-define-property",
-    "core-js-3-only-polyfill/es6-string-iterator",
-    "core-js-3-only-polyfill/es6-string-trim",
     "core-js-3-preset-env-esmodules/true"
   ],
   "variants": [
-    {
-      "name": "core-js-2-only-polyfill/es6-array-copy-within",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-array-every",
-      "signals": ""
-    },
     {
       "name": "core-js-2-only-polyfill/es6-array-fill",
-      "signals": "Array.fill"
+      "signals": "Array.prototype.fill"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-filter",
-      "signals": ""
+      "signals": "Array.prototype.filter"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-find",
@@ -137,25 +29,17 @@
       "name": "core-js-2-only-polyfill/es6-array-from",
       "signals": "Array.from"
     },
-    {
-      "name": "core-js-2-only-polyfill/es6-array-index-of",
-      "signals": ""
-    },
     {
       "name": "core-js-2-only-polyfill/es6-array-is-array",
       "signals": "Array.isArray"
     },
-    {
-      "name": "core-js-2-only-polyfill/es6-array-iterator",
-      "signals": ""
-    },
     {
       "name": "core-js-2-only-polyfill/es6-array-last-index-of",
       "signals": "Array.prototype.lastIndexOf"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-map",
-      "signals": ""
+      "signals": "Array.prototype.map"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-of",
@@ -163,19 +47,15 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-reduce",
-      "signals": ""
+      "signals": "Array.prototype.reduce"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-reduce-right",
-      "signals": ""
+      "signals": "Array.prototype.reduceRight"
     },
     {
       "name": "core-js-2-only-polyfill/es6-array-some",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-array-species",
-      "signals": ""
+      "signals": "Array.prototype.some"
     },
     {
       "name": "core-js-2-only-polyfill/es6-date-now",
@@ -189,106 +69,18 @@
       "name": "core-js-2-only-polyfill/es6-date-to-json",
       "signals": "Date.prototype.toJSON"
     },
-    {
-      "name": "core-js-2-only-polyfill/es6-date-to-primitive",
-      "signals": ""
-    },
     {
       "name": "core-js-2-only-polyfill/es6-date-to-string",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-function-bind",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-function-has-instance",
-      "signals": ""
+      "signals": "Date.prototype.toString"
     },
     {
       "name": "core-js-2-only-polyfill/es6-function-name",
-      "signals": ""
+      "signals": "Function.prototype.name"
     },
     {
       "name": "core-js-2-only-polyfill/es6-map",
       "signals": "Map"
     },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-acosh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-asinh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-atanh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-cbrt",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-clz32",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-cosh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-expm1",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-fround",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-hypot",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-imul",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-log10",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-log1p",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-log2",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-sign",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-sinh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-tanh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-math-trunc",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-number-constructor",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-number-epsilon",
-      "signals": ""
-    },
     {
       "name": "core-js-2-only-polyfill/es6-number-is-integer",
       "signals": "Number.isInteger"
@@ -297,14 +89,6 @@
       "name": "core-js-2-only-polyfill/es6-number-is-safe-integer",
       "signals": "Number.isSafeInteger"
     },
-    {
-      "name": "core-js-2-only-polyfill/es6-number-max-safe-integer",
-      "signals": ""
-    },
-    {
-      "name": "core-js-2-only-polyfill/es6-number-min-safe-integer",
-      "signals": ""
-    },
     {
       "name": "core-js-2-only-polyfill/es6-number-parse-float",
       "signals": "Number.parseFloat"
@@ -331,52 +115,48 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-freeze",
-      "signals": ""
+      "signals": "Object.freeze"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-get-own-property-descriptor",
-      "signals": ""
+      "signals": "Object.getOwnPropertyDescriptor"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-get-own-property-names",
-      "signals": ""
+      "signals": "Object.getOwnPropertyNames"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-get-prototype-of",
-      "signals": ""
+      "signals": "Object.getPrototypeOf"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-is-extensible",
-      "signals": ""
+      "signals": "Object.isExtensible"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-is-frozen",
-      "signals": ""
+      "signals": "Object.isFrozen"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-is-sealed",
-      "signals": ""
+      "signals": "Object.isSealed"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-keys",
-      "signals": ""
+      "signals": "Object.keys"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-prevent-extensions",
-      "signals": ""
+      "signals": "Object.preventExtensions"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-seal",
-      "signals": ""
+      "signals": "Object.seal"
     },
     {
       "name": "core-js-2-only-polyfill/es6-object-set-prototype-of",
       "signals": "Object.setPrototypeOf"
     },
-    {
-      "name": "core-js-2-only-polyfill/es6-object-to-string",
-      "signals": ""
-    },
     {
       "name": "core-js-2-only-polyfill/es6-promise",
       "signals": "Promise"
@@ -391,7 +171,7 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-reflect-define-property",
-      "signals": ""
+      "signals": "Reflect.defineProperty"
     },
     {
       "name": "core-js-2-only-polyfill/es6-reflect-delete-property",
@@ -453,10 +233,6 @@
       "name": "core-js-2-only-polyfill/es6-string-includes",
       "signals": "String.prototype.includes"
     },
-    {
-      "name": "core-js-2-only-polyfill/es6-string-iterator",
-      "signals": ""
-    },
     {
       "name": "core-js-2-only-polyfill/es6-string-raw",
       "signals": "String.raw"
@@ -471,7 +247,7 @@
     },
     {
       "name": "core-js-2-only-polyfill/es6-string-trim",
-      "signals": ""
+      "signals": "String.prototype.trim"
     },
     {
       "name": "core-js-2-only-polyfill/es6-typed-array-buffer",
@@ -557,21 +333,13 @@
       "name": "core-js-2-preset-env-esmodules/true",
       "signals": ""
     },
-    {
-      "name": "core-js-3-only-polyfill/es6-array-copy-within",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-array-every",
-      "signals": ""
-    },
     {
       "name": "core-js-3-only-polyfill/es6-array-fill",
-      "signals": "Array.fill"
+      "signals": "Array.prototype.fill"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-filter",
-      "signals": ""
+      "signals": "Array.prototype.filter"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-find",
@@ -589,25 +357,17 @@
       "name": "core-js-3-only-polyfill/es6-array-from",
       "signals": "Array.from"
     },
-    {
-      "name": "core-js-3-only-polyfill/es6-array-index-of",
-      "signals": ""
-    },
     {
       "name": "core-js-3-only-polyfill/es6-array-is-array",
       "signals": "Array.isArray"
     },
-    {
-      "name": "core-js-3-only-polyfill/es6-array-iterator",
-      "signals": ""
-    },
     {
       "name": "core-js-3-only-polyfill/es6-array-last-index-of",
       "signals": "Array.prototype.lastIndexOf"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-map",
-      "signals": ""
+      "signals": "Array.prototype.map"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-of",
@@ -615,19 +375,15 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-reduce",
-      "signals": ""
+      "signals": "Array.prototype.reduce"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-reduce-right",
-      "signals": ""
+      "signals": "Array.prototype.reduceRight"
     },
     {
       "name": "core-js-3-only-polyfill/es6-array-some",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-array-species",
-      "signals": ""
+      "signals": "Array.prototype.some"
     },
     {
       "name": "core-js-3-only-polyfill/es6-date-now",
@@ -641,106 +397,18 @@
       "name": "core-js-3-only-polyfill/es6-date-to-json",
       "signals": "Date.prototype.toJSON"
     },
-    {
-      "name": "core-js-3-only-polyfill/es6-date-to-primitive",
-      "signals": ""
-    },
     {
       "name": "core-js-3-only-polyfill/es6-date-to-string",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-function-bind",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-function-has-instance",
-      "signals": ""
+      "signals": "Date.prototype.toString"
     },
     {
       "name": "core-js-3-only-polyfill/es6-function-name",
-      "signals": ""
+      "signals": "Function.prototype.name"
     },
     {
       "name": "core-js-3-only-polyfill/es6-map",
       "signals": "Map"
     },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-acosh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-asinh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-atanh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-cbrt",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-clz32",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-cosh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-expm1",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-fround",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-hypot",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-imul",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-log10",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-log1p",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-log2",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-sign",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-sinh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-tanh",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-math-trunc",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-number-constructor",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-number-epsilon",
-      "signals": ""
-    },
     {
       "name": "core-js-3-only-polyfill/es6-number-is-integer",
       "signals": "Number.isInteger"
@@ -749,14 +417,6 @@
       "name": "core-js-3-only-polyfill/es6-number-is-safe-integer",
       "signals": "Number.isSafeInteger"
     },
-    {
-      "name": "core-js-3-only-polyfill/es6-number-max-safe-integer",
-      "signals": ""
-    },
-    {
-      "name": "core-js-3-only-polyfill/es6-number-min-safe-integer",
-      "signals": ""
-    },
     {
       "name": "core-js-3-only-polyfill/es6-number-parse-float",
       "signals": "Number.parseFloat"
@@ -783,52 +443,48 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-freeze",
-      "signals": ""
+      "signals": "Object.freeze"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-get-own-property-descriptor",
-      "signals": ""
+      "signals": "Object.getOwnPropertyDescriptor"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-get-own-property-names",
-      "signals": ""
+      "signals": "Object.getOwnPropertyNames"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-get-prototype-of",
-      "signals": ""
+      "signals": "Object.getPrototypeOf"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-is-extensible",
-      "signals": ""
+      "signals": "Object.isExtensible"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-is-frozen",
-      "signals": ""
+      "signals": "Object.isFrozen"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-is-sealed",
-      "signals": ""
+      "signals": "Object.isSealed"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-keys",
-      "signals": ""
+      "signals": "Object.keys"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-prevent-extensions",
-      "signals": ""
+      "signals": "Object.preventExtensions"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-seal",
-      "signals": ""
+      "signals": "Object.seal"
     },
     {
       "name": "core-js-3-only-polyfill/es6-object-set-prototype-of",
       "signals": "Object.setPrototypeOf"
     },
-    {
-      "name": "core-js-3-only-polyfill/es6-object-to-string",
-      "signals": ""
-    },
     {
       "name": "core-js-3-only-polyfill/es6-promise",
       "signals": "Promise"
@@ -843,7 +499,7 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-reflect-define-property",
-      "signals": ""
+      "signals": "Reflect.defineProperty"
     },
     {
       "name": "core-js-3-only-polyfill/es6-reflect-delete-property",
@@ -905,10 +561,6 @@
       "name": "core-js-3-only-polyfill/es6-string-includes",
       "signals": "String.prototype.includes"
     },
-    {
-      "name": "core-js-3-only-polyfill/es6-string-iterator",
-      "signals": ""
-    },
     {
       "name": "core-js-3-only-polyfill/es6-string-raw",
       "signals": "String.raw"
@@ -923,7 +575,7 @@
     },
     {
       "name": "core-js-3-only-polyfill/es6-string-trim",
-      "signals": ""
+      "signals": "String.prototype.trim"
     },
     {
       "name": "core-js-3-only-polyfill/es6-typed-array-buffer",
diff --git a/lighthouse-core/scripts/legacy-javascript/summary-sizes.txt b/lighthouse-core/scripts/legacy-javascript/summary-sizes.txt
index 2aa40a40e..9fb2da643 100644
--- a/lighthouse-core/scripts/legacy-javascript/summary-sizes.txt
+++ b/lighthouse-core/scripts/legacy-javascript/summary-sizes.txt
@@ -1,238 +1,176 @@
 core-js-2-only-polyfill
-  47738        es6-typed-uint8-clamped-array/main.bundle.min.js
-  47718        es6-typed-float64-array/main.bundle.min.js
-  47718        es6-typed-float32-array/main.bundle.min.js
-  47714        es6-typed-uint32-array/main.bundle.min.js
-  47714        es6-typed-uint16-array/main.bundle.min.js
-  47710        es6-typed-uint8-array/main.bundle.min.js
-  47710        es6-typed-int32-array/main.bundle.min.js
-  47710        es6-typed-int16-array/main.bundle.min.js
-  47706        es6-typed-int8-array/main.bundle.min.js
-  29514        es6-map/main.bundle.min.js
-  29420        es6-set/main.bundle.min.js
-  27277        es6-weak-map/main.bundle.min.js
-  24645        es6-promise/main.bundle.min.js
-  23127        es6-typed-array-buffer/main.bundle.min.js
-  22881        es6-weak-set/main.bundle.min.js
-  20885        es6-typed-data-view/main.bundle.min.js
-  17640        es6-array-iterator/main.bundle.min.js
-  17387        es6-number-constructor/main.bundle.min.js
-  17329        es6-string-iterator/main.bundle.min.js
-  14880        es6-reflect-construct/main.bundle.min.js
-  12738        es7-object-get-own-property-descriptors/main.bundle.min.js
-  12329        es6-object-create/main.bundle.min.js
-  12184        es6-array-from/main.bundle.min.js
-  11963        es6-object-assign/main.bundle.min.js
-  11575        es6-object-get-own-property-names/main.bundle.min.js
-  11412        es6-array-find-index/main.bundle.min.js
-  11385        es6-array-find/main.bundle.min.js
-  11295        es7-object-entries/main.bundle.min.js
-  11290        es7-object-values/main.bundle.min.js
-  11240        es6-array-for-each/main.bundle.min.js
-  11213        es6-array-filter/main.bundle.min.js
-  11206        es6-array-every/main.bundle.min.js
-  11199        es6-array-some/main.bundle.min.js
-  11192        es6-array-map/main.bundle.min.js
-  11187        es6-object-keys/main.bundle.min.js
-  11159        es6-reflect-own-keys/main.bundle.min.js
-  11146        es6-object-define-properties/main.bundle.min.js
-  10840        es6-reflect-set/main.bundle.min.js
-  10362        es6-reflect-get/main.bundle.min.js
-  10323        es7-array-includes/main.bundle.min.js
-   9992        es6-array-index-of/main.bundle.min.js
-   9974        es6-string-ends-with/main.bundle.min.js
-   9928        es6-string-starts-with/main.bundle.min.js
-   9916        es6-array-copy-within/main.bundle.min.js
-   9840        es6-reflect-set-prototype-of/main.bundle.min.js
-   9726        es6-array-fill/main.bundle.min.js
-   9706        es6-array-reduce-right/main.bundle.min.js
-   9682        es6-object-set-prototype-of/main.bundle.min.js
-   9680        es6-array-reduce/main.bundle.min.js
-   9561        es7-string-pad-start/main.bundle.min.js
-   9554        es7-string-pad-end/main.bundle.min.js
-   9532        es6-object-get-own-property-descriptor/main.bundle.min.js
-   9458        es6-array-last-index-of/main.bundle.min.js
-   9315        es6-string-includes/main.bundle.min.js
-   9196        es6-reflect-delete-property/main.bundle.min.js
-   9173        es6-reflect-get-own-property-descriptor/main.bundle.min.js
-   9120        es6-object-prevent-extensions/main.bundle.min.js
-   9093        es6-number-parse-int/main.bundle.min.js
-   9087        es6-number-parse-float/main.bundle.min.js
-   9041        es6-object-freeze/main.bundle.min.js
-   9027        es6-object-seal/main.bundle.min.js
-   9009        es6-object-get-prototype-of/main.bundle.min.js
-   8843        es6-string-raw/main.bundle.min.js
-   8792        es6-function-bind/main.bundle.min.js
-   8667        es6-reflect-get-prototype-of/main.bundle.min.js
-   8593        es6-string-trim/main.bundle.min.js
-   8445        es6-string-from-code-point/main.bundle.min.js
-   8423        es6-date-to-iso-string/main.bundle.min.js
-   8413        es6-string-code-point-at/main.bundle.min.js
-   8219        es6-math-fround/main.bundle.min.js
-   8217        es6-string-repeat/main.bundle.min.js
-   8193        es6-array-of/main.bundle.min.js
-   8191        es6-date-to-json/main.bundle.min.js
-   8004        es6-reflect-define-property/main.bundle.min.js
-   7998        es6-object-is-extensible/main.bundle.min.js
-   7984        es6-math-sinh/main.bundle.min.js
-   7970        es6-object-is-sealed/main.bundle.min.js
-   7970        es6-object-is-frozen/main.bundle.min.js
-   7942        es6-reflect-apply/main.bundle.min.js
-   7907        es6-math-tanh/main.bundle.min.js
-   7906        es6-math-acosh/main.bundle.min.js
-   7876        es6-number-is-safe-integer/main.bundle.min.js
-   7821        es6-math-expm1/main.bundle.min.js
-   7814        es6-array-is-array/main.bundle.min.js
-   7786        es6-math-hypot/main.bundle.min.js
-   7763        es6-reflect-prevent-extensions/main.bundle.min.js
-   7752        es6-math-imul/main.bundle.min.js
-   7743        es6-number-is-integer/main.bundle.min.js
-   7702        es6-math-cbrt/main.bundle.min.js
-   7693        es6-reflect-is-extensible/main.bundle.min.js
-   7654        es6-math-log1p/main.bundle.min.js
-   7631        es6-math-sign/main.bundle.min.js
-   7628        es6-math-asinh/main.bundle.min.js
-   7608        es6-object-define-property/main.bundle.min.js
-   7592        es6-math-atanh/main.bundle.min.js
-   7551        es6-math-clz32/main.bundle.min.js
-   7539        es6-reflect-has/main.bundle.min.js
-   7533        es6-math-cosh/main.bundle.min.js
-   7531        es6-math-trunc/main.bundle.min.js
-   7530        es6-number-min-safe-integer/main.bundle.min.js
-   7529        es6-number-max-safe-integer/main.bundle.min.js
-   7523        es6-math-log10/main.bundle.min.js
-   7516        es6-math-log2/main.bundle.min.js
-   7506        es6-date-now/main.bundle.min.js
-   7501        es6-number-epsilon/main.bundle.min.js
-   6915        es6-object-to-string/main.bundle.min.js
-   5994        es6-function-has-instance/main.bundle.min.js
-   5993        es6-date-to-string/main.bundle.min.js
-   5422        es6-date-to-primitive/main.bundle.min.js
-   4774        es6-array-species/main.bundle.min.js
-   3410        es6-function-name/main.bundle.min.js
+  47782        es6-typed-uint8-clamped-array/main.bundle.min.js
+  47762        es6-typed-float64-array/main.bundle.min.js
+  47762        es6-typed-float32-array/main.bundle.min.js
+  47758        es6-typed-uint32-array/main.bundle.min.js
+  47758        es6-typed-uint16-array/main.bundle.min.js
+  47754        es6-typed-uint8-array/main.bundle.min.js
+  47754        es6-typed-int32-array/main.bundle.min.js
+  47754        es6-typed-int16-array/main.bundle.min.js
+  47750        es6-typed-int8-array/main.bundle.min.js
+  29558        es6-map/main.bundle.min.js
+  29464        es6-set/main.bundle.min.js
+  27321        es6-weak-map/main.bundle.min.js
+  24689        es6-promise/main.bundle.min.js
+  23171        es6-typed-array-buffer/main.bundle.min.js
+  22925        es6-weak-set/main.bundle.min.js
+  20929        es6-typed-data-view/main.bundle.min.js
+  14924        es6-reflect-construct/main.bundle.min.js
+  12782        es7-object-get-own-property-descriptors/main.bundle.min.js
+  12373        es6-object-create/main.bundle.min.js
+  12228        es6-array-from/main.bundle.min.js
+  12007        es6-object-assign/main.bundle.min.js
+  11619        es6-object-get-own-property-names/main.bundle.min.js
+  11456        es6-array-find-index/main.bundle.min.js
+  11429        es6-array-find/main.bundle.min.js
+  11339        es7-object-entries/main.bundle.min.js
+  11334        es7-object-values/main.bundle.min.js
+  11284        es6-array-for-each/main.bundle.min.js
+  11257        es6-array-filter/main.bundle.min.js
+  11243        es6-array-some/main.bundle.min.js
+  11236        es6-array-map/main.bundle.min.js
+  11231        es6-object-keys/main.bundle.min.js
+  11203        es6-reflect-own-keys/main.bundle.min.js
+  11190        es6-object-define-properties/main.bundle.min.js
+  10884        es6-reflect-set/main.bundle.min.js
+  10406        es6-reflect-get/main.bundle.min.js
+  10367        es7-array-includes/main.bundle.min.js
+  10018        es6-string-ends-with/main.bundle.min.js
+   9972        es6-string-starts-with/main.bundle.min.js
+   9884        es6-reflect-set-prototype-of/main.bundle.min.js
+   9770        es6-array-fill/main.bundle.min.js
+   9750        es6-array-reduce-right/main.bundle.min.js
+   9726        es6-object-set-prototype-of/main.bundle.min.js
+   9724        es6-array-reduce/main.bundle.min.js
+   9605        es7-string-pad-start/main.bundle.min.js
+   9598        es7-string-pad-end/main.bundle.min.js
+   9576        es6-object-get-own-property-descriptor/main.bundle.min.js
+   9502        es6-array-last-index-of/main.bundle.min.js
+   9359        es6-string-includes/main.bundle.min.js
+   9240        es6-reflect-delete-property/main.bundle.min.js
+   9217        es6-reflect-get-own-property-descriptor/main.bundle.min.js
+   9164        es6-object-prevent-extensions/main.bundle.min.js
+   9137        es6-number-parse-int/main.bundle.min.js
+   9131        es6-number-parse-float/main.bundle.min.js
+   9085        es6-object-freeze/main.bundle.min.js
+   9071        es6-object-seal/main.bundle.min.js
+   9053        es6-object-get-prototype-of/main.bundle.min.js
+   8887        es6-string-raw/main.bundle.min.js
+   8711        es6-reflect-get-prototype-of/main.bundle.min.js
+   8637        es6-string-trim/main.bundle.min.js
+   8489        es6-string-from-code-point/main.bundle.min.js
+   8467        es6-date-to-iso-string/main.bundle.min.js
+   8457        es6-string-code-point-at/main.bundle.min.js
+   8261        es6-string-repeat/main.bundle.min.js
+   8237        es6-array-of/main.bundle.min.js
+   8235        es6-date-to-json/main.bundle.min.js
+   8048        es6-reflect-define-property/main.bundle.min.js
+   8042        es6-object-is-extensible/main.bundle.min.js
+   8014        es6-object-is-sealed/main.bundle.min.js
+   8014        es6-object-is-frozen/main.bundle.min.js
+   7986        es6-reflect-apply/main.bundle.min.js
+   7920        es6-number-is-safe-integer/main.bundle.min.js
+   7858        es6-array-is-array/main.bundle.min.js
+   7807        es6-reflect-prevent-extensions/main.bundle.min.js
+   7787        es6-number-is-integer/main.bundle.min.js
+   7737        es6-reflect-is-extensible/main.bundle.min.js
+   7652        es6-object-define-property/main.bundle.min.js
+   7583        es6-reflect-has/main.bundle.min.js
+   7550        es6-date-now/main.bundle.min.js
+   6037        es6-date-to-string/main.bundle.min.js
+   3454        es6-function-name/main.bundle.min.js
 
 core-js-2-preset-env-esmodules
-  2813         false/main.bundle.min.js
-  1308         true/main.bundle.min.js
+  2857         false/main.bundle.min.js
+  1352         true/main.bundle.min.js
 
 core-js-3-only-polyfill
-  47737        es6-typed-uint8-clamped-array/main.bundle.min.js
-  47717        es6-typed-float64-array/main.bundle.min.js
-  47717        es6-typed-float32-array/main.bundle.min.js
-  47713        es6-typed-uint32-array/main.bundle.min.js
-  47713        es6-typed-uint16-array/main.bundle.min.js
-  47709        es6-typed-uint8-array/main.bundle.min.js
-  47709        es6-typed-int32-array/main.bundle.min.js
-  47709        es6-typed-int16-array/main.bundle.min.js
-  47705        es6-typed-int8-array/main.bundle.min.js
-  29516        es6-map/main.bundle.min.js
-  29422        es6-set/main.bundle.min.js
-  27278        es6-weak-map/main.bundle.min.js
-  24645        es6-promise/main.bundle.min.js
-  23127        es6-typed-array-buffer/main.bundle.min.js
-  22880        es6-weak-set/main.bundle.min.js
-  20885        es6-typed-data-view/main.bundle.min.js
-  17642        es6-array-iterator/main.bundle.min.js
-  17387        es6-number-constructor/main.bundle.min.js
-  17329        es6-string-iterator/main.bundle.min.js
-  14882        es6-reflect-construct/main.bundle.min.js
-  12740        es7-object-get-own-property-descriptors/main.bundle.min.js
-  12329        es6-object-create/main.bundle.min.js
-  12185        es6-array-from/main.bundle.min.js
-  11962        es6-object-assign/main.bundle.min.js
-  11574        es6-object-get-own-property-names/main.bundle.min.js
-  11412        es6-array-find-index/main.bundle.min.js
-  11385        es6-array-find/main.bundle.min.js
-  11294        es7-object-entries/main.bundle.min.js
-  11289        es7-object-values/main.bundle.min.js
-  11239        es6-array-for-each/main.bundle.min.js
-  11212        es6-array-filter/main.bundle.min.js
-  11205        es6-array-every/main.bundle.min.js
-  11198        es6-array-some/main.bundle.min.js
-  11191        es6-array-map/main.bundle.min.js
-  11186        es6-object-keys/main.bundle.min.js
-  11158        es6-reflect-own-keys/main.bundle.min.js
-  11145        es6-object-define-properties/main.bundle.min.js
-  10839        es6-reflect-set/main.bundle.min.js
-  10361        es6-reflect-get/main.bundle.min.js
-  10324        es7-array-includes/main.bundle.min.js
-   9991        es6-array-index-of/main.bundle.min.js
-   9973        es6-string-ends-with/main.bundle.min.js
-   9927        es6-string-starts-with/main.bundle.min.js
-   9915        es6-array-copy-within/main.bundle.min.js
-   9839        es6-reflect-set-prototype-of/main.bundle.min.js
-   9725        es6-array-fill/main.bundle.min.js
-   9705        es6-array-reduce-right/main.bundle.min.js
-   9681        es6-object-set-prototype-of/main.bundle.min.js
-   9679        es6-array-reduce/main.bundle.min.js
-   9561        es7-string-pad-start/main.bundle.min.js
-   9554        es7-string-pad-end/main.bundle.min.js
-   9531        es6-object-get-own-property-descriptor/main.bundle.min.js
-   9457        es6-array-last-index-of/main.bundle.min.js
-   9314        es6-string-includes/main.bundle.min.js
-   9195        es6-reflect-delete-property/main.bundle.min.js
-   9172        es6-reflect-get-own-property-descriptor/main.bundle.min.js
-   9119        es6-object-prevent-extensions/main.bundle.min.js
-   9094        es6-number-parse-int/main.bundle.min.js
-   9088        es6-number-parse-float/main.bundle.min.js
-   9040        es6-object-freeze/main.bundle.min.js
-   9026        es6-object-seal/main.bundle.min.js
-   9010        es6-object-get-prototype-of/main.bundle.min.js
-   8842        es6-string-raw/main.bundle.min.js
-   8792        es6-function-bind/main.bundle.min.js
-   8667        es6-reflect-get-prototype-of/main.bundle.min.js
-   8594        es6-string-trim/main.bundle.min.js
-   8444        es6-string-from-code-point/main.bundle.min.js
-   8424        es6-date-to-iso-string/main.bundle.min.js
-   8413        es6-string-code-point-at/main.bundle.min.js
-   8218        es6-math-fround/main.bundle.min.js
-   8217        es6-string-repeat/main.bundle.min.js
-   8194        es6-array-of/main.bundle.min.js
-   8192        es6-date-to-json/main.bundle.min.js
-   8003        es6-reflect-define-property/main.bundle.min.js
-   7997        es6-object-is-extensible/main.bundle.min.js
-   7983        es6-math-sinh/main.bundle.min.js
-   7969        es6-object-is-sealed/main.bundle.min.js
-   7969        es6-object-is-frozen/main.bundle.min.js
-   7941        es6-reflect-apply/main.bundle.min.js
-   7906        es6-math-tanh/main.bundle.min.js
-   7905        es6-math-acosh/main.bundle.min.js
-   7875        es6-number-is-safe-integer/main.bundle.min.js
-   7820        es6-math-expm1/main.bundle.min.js
-   7814        es6-array-is-array/main.bundle.min.js
-   7785        es6-math-hypot/main.bundle.min.js
-   7762        es6-reflect-prevent-extensions/main.bundle.min.js
-   7751        es6-math-imul/main.bundle.min.js
-   7742        es6-number-is-integer/main.bundle.min.js
-   7701        es6-math-cbrt/main.bundle.min.js
-   7692        es6-reflect-is-extensible/main.bundle.min.js
-   7653        es6-math-log1p/main.bundle.min.js
-   7630        es6-math-sign/main.bundle.min.js
-   7627        es6-math-asinh/main.bundle.min.js
-   7607        es6-object-define-property/main.bundle.min.js
-   7591        es6-math-atanh/main.bundle.min.js
-   7550        es6-math-clz32/main.bundle.min.js
-   7538        es6-reflect-has/main.bundle.min.js
-   7532        es6-math-cosh/main.bundle.min.js
-   7530        es6-math-trunc/main.bundle.min.js
-   7529        es6-number-min-safe-integer/main.bundle.min.js
-   7528        es6-number-max-safe-integer/main.bundle.min.js
-   7522        es6-math-log10/main.bundle.min.js
-   7515        es6-math-log2/main.bundle.min.js
-   7505        es6-date-now/main.bundle.min.js
-   7500        es6-number-epsilon/main.bundle.min.js
-   6917        es6-object-to-string/main.bundle.min.js
-   5993        es6-function-has-instance/main.bundle.min.js
-   5992        es6-date-to-string/main.bundle.min.js
-   5421        es6-date-to-primitive/main.bundle.min.js
-   4773        es6-array-species/main.bundle.min.js
-   3410        es6-function-name/main.bundle.min.js
+  47781        es6-typed-uint8-clamped-array/main.bundle.min.js
+  47761        es6-typed-float64-array/main.bundle.min.js
+  47761        es6-typed-float32-array/main.bundle.min.js
+  47757        es6-typed-uint32-array/main.bundle.min.js
+  47757        es6-typed-uint16-array/main.bundle.min.js
+  47753        es6-typed-uint8-array/main.bundle.min.js
+  47753        es6-typed-int32-array/main.bundle.min.js
+  47753        es6-typed-int16-array/main.bundle.min.js
+  47749        es6-typed-int8-array/main.bundle.min.js
+  29560        es6-map/main.bundle.min.js
+  29466        es6-set/main.bundle.min.js
+  27322        es6-weak-map/main.bundle.min.js
+  24689        es6-promise/main.bundle.min.js
+  23171        es6-typed-array-buffer/main.bundle.min.js
+  22924        es6-weak-set/main.bundle.min.js
+  20929        es6-typed-data-view/main.bundle.min.js
+  14926        es6-reflect-construct/main.bundle.min.js
+  12784        es7-object-get-own-property-descriptors/main.bundle.min.js
+  12373        es6-object-create/main.bundle.min.js
+  12229        es6-array-from/main.bundle.min.js
+  12006        es6-object-assign/main.bundle.min.js
+  11618        es6-object-get-own-property-names/main.bundle.min.js
+  11456        es6-array-find-index/main.bundle.min.js
+  11429        es6-array-find/main.bundle.min.js
+  11338        es7-object-entries/main.bundle.min.js
+  11333        es7-object-values/main.bundle.min.js
+  11283        es6-array-for-each/main.bundle.min.js
+  11256        es6-array-filter/main.bundle.min.js
+  11242        es6-array-some/main.bundle.min.js
+  11235        es6-array-map/main.bundle.min.js
+  11230        es6-object-keys/main.bundle.min.js
+  11202        es6-reflect-own-keys/main.bundle.min.js
+  11189        es6-object-define-properties/main.bundle.min.js
+  10883        es6-reflect-set/main.bundle.min.js
+  10405        es6-reflect-get/main.bundle.min.js
+  10368        es7-array-includes/main.bundle.min.js
+  10017        es6-string-ends-with/main.bundle.min.js
+   9971        es6-string-starts-with/main.bundle.min.js
+   9883        es6-reflect-set-prototype-of/main.bundle.min.js
+   9769        es6-array-fill/main.bundle.min.js
+   9749        es6-array-reduce-right/main.bundle.min.js
+   9725        es6-object-set-prototype-of/main.bundle.min.js
+   9723        es6-array-reduce/main.bundle.min.js
+   9605        es7-string-pad-start/main.bundle.min.js
+   9598        es7-string-pad-end/main.bundle.min.js
+   9575        es6-object-get-own-property-descriptor/main.bundle.min.js
+   9501        es6-array-last-index-of/main.bundle.min.js
+   9358        es6-string-includes/main.bundle.min.js
+   9239        es6-reflect-delete-property/main.bundle.min.js
+   9216        es6-reflect-get-own-property-descriptor/main.bundle.min.js
+   9163        es6-object-prevent-extensions/main.bundle.min.js
+   9138        es6-number-parse-int/main.bundle.min.js
+   9132        es6-number-parse-float/main.bundle.min.js
+   9084        es6-object-freeze/main.bundle.min.js
+   9070        es6-object-seal/main.bundle.min.js
+   9054        es6-object-get-prototype-of/main.bundle.min.js
+   8886        es6-string-raw/main.bundle.min.js
+   8711        es6-reflect-get-prototype-of/main.bundle.min.js
+   8638        es6-string-trim/main.bundle.min.js
+   8488        es6-string-from-code-point/main.bundle.min.js
+   8468        es6-date-to-iso-string/main.bundle.min.js
+   8457        es6-string-code-point-at/main.bundle.min.js
+   8261        es6-string-repeat/main.bundle.min.js
+   8238        es6-array-of/main.bundle.min.js
+   8236        es6-date-to-json/main.bundle.min.js
+   8047        es6-reflect-define-property/main.bundle.min.js
+   8041        es6-object-is-extensible/main.bundle.min.js
+   8013        es6-object-is-sealed/main.bundle.min.js
+   8013        es6-object-is-frozen/main.bundle.min.js
+   7985        es6-reflect-apply/main.bundle.min.js
+   7919        es6-number-is-safe-integer/main.bundle.min.js
+   7858        es6-array-is-array/main.bundle.min.js
+   7806        es6-reflect-prevent-extensions/main.bundle.min.js
+   7786        es6-number-is-integer/main.bundle.min.js
+   7736        es6-reflect-is-extensible/main.bundle.min.js
+   7651        es6-object-define-property/main.bundle.min.js
+   7582        es6-reflect-has/main.bundle.min.js
+   7549        es6-date-now/main.bundle.min.js
+   6036        es6-date-to-string/main.bundle.min.js
+   3454        es6-function-name/main.bundle.min.js
 
 core-js-3-preset-env-esmodules
-  2813         false/main.bundle.min.js
-  1308         true/main.bundle.min.js
+  2857         false/main.bundle.min.js
+  1352         true/main.bundle.min.js
 
 only-plugin
-  1706         -babel-plugin-transform-spread/main.bundle.min.js
-   827         -babel-plugin-transform-regenerator/main.bundle.min.js
-   814         -babel-plugin-transform-classes/main.bundle.min.js
+  1750         -babel-plugin-transform-spread/main.bundle.min.js
+   871         -babel-plugin-transform-regenerator/main.bundle.min.js
+   858         -babel-plugin-transform-classes/main.bundle.min.js
diff --git a/lighthouse-viewer/README.md b/lighthouse-viewer/README.md
index 7adc9ad46..01d023c53 100644
--- a/lighthouse-viewer/README.md
+++ b/lighthouse-viewer/README.md
@@ -21,20 +21,28 @@ yarn deploy-viewer
 
 For more information on deployment, see `releasing.md`.
 
-## Gist
+## Usage
 
-http://localhost:8000/?gist=bd1779783a5bbcb348564a58f80f7099
+### Load JSON from Gist
 
-## PSI
+Pass the GitHub Gist identifier as `gist` query parameter.
 
-Example:
-```
-http://localhost:8000/?psiurl=https://www.example.com&category=pwa&category=seo
-```
+e.g., `http://localhost:8000/?gist=bd1779783a5bbcb348564a58f80f7099`
+
+### Load JSON from URL
+
+Pass the absolute URL as `jsonurl` query parameter.
+
+e.g., `http://localhost:8000/?josnurl=https://gist.githubusercontent.com/Kikobeats/d570a1aa285c5d1d97bbda10b92fb97f/raw/4b0f14a5914edd25c95b4bd9d09728ab42181c3e/lighthouse.json`
+
+### Run and load from PageSpeed Insights
+
+Pass target URL `psiurl` query parameter.
+
+e.g., `http://localhost:8000/?psiurl=https://www.example.com&category=pwa&category=seo`
 
-Options:
+The following query parameters are also supported as options:
 
-`psiurl` - URL to audit
 `category` - Category to enable. One per category.
 `strategy` - mobile, desktop
 `locale` - locale to render report with
diff --git a/lighthouse-viewer/app/src/github-api.js b/lighthouse-viewer/app/src/github-api.js
index eaad39926..4d9ce6f59 100644
--- a/lighthouse-viewer/app/src/github-api.js
+++ b/lighthouse-viewer/app/src/github-api.js
@@ -22,6 +22,10 @@ class GithubApi {
     return '.lighthouse.report.json';
   }
 
+  getFirebaseAuth() {
+    return this._auth;
+  }
+
   /**
    * Creates a gist under the users account.
    * @param {LH.Result} jsonFile The gist file body.
diff --git a/lighthouse-viewer/app/src/lighthouse-report-viewer.js b/lighthouse-viewer/app/src/lighthouse-report-viewer.js
index 7f6e4f384..b054096ff 100644
--- a/lighthouse-viewer/app/src/lighthouse-report-viewer.js
+++ b/lighthouse-viewer/app/src/lighthouse-report-viewer.js
@@ -45,6 +45,7 @@ class LighthouseReportViewer {
      */
     this._reportIsFromGist = false;
     this._reportIsFromPSI = false;
+    this._reportIsFromJSON = false;
 
     this._addEventListeners();
     this._loadFromDeepLink();
@@ -100,8 +101,9 @@ class LighthouseReportViewer {
 
     const gistId = params.get('gist');
     const psiurl = params.get('psiurl');
+    const jsonurl = params.get('jsonurl');
 
-    if (!gistId && !psiurl) return Promise.resolve();
+    if (!gistId && !psiurl && !jsonurl) return Promise.resolve();
 
     this._toggleLoadingBlur(true);
     let loadPromise = Promise.resolve();
@@ -118,6 +120,21 @@ class LighthouseReportViewer {
         this._reportIsFromGist = true;
         this._replaceReportHtml(reportJson);
       }).catch(err => logger.error(err.message));
+    } else if (jsonurl) {
+      const firebaseAuth = this._github.getFirebaseAuth();
+      loadPromise = firebaseAuth.getAccessTokenIfLoggedIn()
+        .then(token => {
+          return token
+            ? Promise.reject(new Error('Can only use jsonurl when not logged in'))
+            : null;
+        })
+        .then(() => fetch(jsonurl))
+        .then(resp => resp.json())
+        .then(json => {
+          this._reportIsFromJSON = true;
+          this._replaceReportHtml(json);
+        })
+        .catch(err => logger.error(err.message));
     }
 
     return loadPromise.finally(() => this._toggleLoadingBlur(false));
@@ -184,7 +201,7 @@ class LighthouseReportViewer {
       }
 
       // Only clear query string if current report isn't from a gist or PSI.
-      if (!this._reportIsFromGist && !this._reportIsFromPSI) {
+      if (!this._reportIsFromGist && !this._reportIsFromPSI && !this._reportIsFromJSON) {
         history.pushState({}, '', LighthouseReportViewer.APP_URL);
       }
 
@@ -196,7 +213,7 @@ class LighthouseReportViewer {
       container.textContent = '';
       throw e;
     } finally {
-      this._reportIsFromGist = this._reportIsFromPSI = false;
+      this._reportIsFromGist = this._reportIsFromPSI = this._reportIsFromJSON = false;
     }
 
     // Remove the placeholder UI once the user has loaded a report.
diff --git a/package.json b/package.json
index 24d2888cd..55ddf3625 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "build-extension": "yarn build-extension-chrome && yarn build-extension-firefox",
     "build-extension-chrome": "node ./build/build-extension.js chrome",
     "build-extension-firefox": "node ./build/build-extension.js firefox",
-    "build-devtools": "yarn link && yarn link lighthouse && node ./build/build-bundle.js clients/devtools-entry.js dist/lighthouse-dt-bundle.js && node ./build/build-dt-report-resources.js",
+    "build-devtools": "(yarn unlink || true) && yarn link && yarn link lighthouse && node ./build/build-bundle.js clients/devtools-entry.js dist/lighthouse-dt-bundle.js && node ./build/build-dt-report-resources.js",
     "build-smokehouse-bundle": "node ./build/build-smokehouse-bundle.js",
     "build-lr": "node ./build/build-lightrider-bundles.js",
     "build-pack": "bash build/build-pack.sh",
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index 6602a1c6c..8564cf053 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -114,8 +114,8 @@ declare global {
       HTMLWithoutJavaScript: {bodyText: string, hasNoScript: boolean};
       /** Whether the page ended up on an HTTPS page after attempting to load the HTTP version. */
       HTTPRedirect: {value: boolean};
-      /** JS coverage information for code used during page load. */
-      JsUsage: Crdp.Profiler.ScriptCoverage[];
+      /** JS coverage information for code used during page load. Keyed by URL. */
+      JsUsage: Record<string, Crdp.Profiler.ScriptCoverage[]>;
       /** Parsed version of the page's Web App Manifest, or null if none found. */
       Manifest: Artifacts.Manifest | null;
       /** The URL loaded with interception */
@@ -304,6 +304,7 @@ declare global {
         script: ScriptElement;
         map: TextSourceMap;
         sizes: {
+          // TODO(cjamcl): Rename to `sources`.
           files: Record<string, number>;
           unmappedBytes: number;
           totalBytes: number;
diff --git a/types/budget.d.ts b/types/budget.d.ts
index 92188aa3b..9fdfe8791 100644
--- a/types/budget.d.ts
+++ b/types/budget.d.ts
@@ -53,7 +53,7 @@ declare global {
       }
 
       /** Supported timing metrics. */
-      export type TimingMetric = 'first-contentful-paint' | 'first-cpu-idle' | 'interactive' | 'first-meaningful-paint' | 'max-potential-fid' | 'estimated-input-latency' | 'total-blocking-time' | 'speed-index';
+      export type TimingMetric = 'first-contentful-paint' | 'first-cpu-idle' | 'interactive' | 'first-meaningful-paint' | 'max-potential-fid' | 'estimated-input-latency' | 'total-blocking-time' | 'speed-index' | 'largest-contentful-paint' | 'cumulative-layout-shift';
 
       /** Supported values for the resourceType property of a ResourceBudget. */
       export type ResourceType = 'stylesheet' | 'image' | 'media' | 'font' | 'script' | 'document' | 'other' | 'total' | 'third-party';


Test patch:
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/legacy-javascript/legacy-javascript-config.js b/lighthouse-cli/test/smokehouse/test-definitions/legacy-javascript/legacy-javascript-config.js
index 20053469b..de04d3769 100644
--- a/lighthouse-cli/test/smokehouse/test-definitions/legacy-javascript/legacy-javascript-config.js
+++ b/lighthouse-cli/test/smokehouse/test-definitions/legacy-javascript/legacy-javascript-config.js
@@ -10,6 +10,12 @@
  */
 module.exports = {
   extends: 'lighthouse:default',
+  passes: [{
+    passName: 'defaultPass',
+    gatherers: [
+      'source-maps',
+    ],
+  }],
   settings: {
     onlyCategories: [
       'performance',
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/pwa/pwa3-expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/pwa/pwa3-expectations.js
index eb211c5b6..27bc60776 100644
--- a/lighthouse-cli/test/smokehouse/test-definitions/pwa/pwa3-expectations.js
+++ b/lighthouse-cli/test/smokehouse/test-definitions/pwa/pwa3-expectations.js
@@ -16,8 +16,10 @@ const pwaRocksExpectations = {...pwaDetailsExpectations, hasIconsAtLeast512px: f
 module.exports = [
   {
     lhr: {
-      requestedUrl: 'https://pwa.rocks',
-      finalUrl: 'https://pwa.rocks/',
+      // Archived version of https://github.com/pwarocks/pwa.rocks
+      // Fork is here: https://github.com/connorjclark/pwa.rocks
+      requestedUrl: 'https://connorjclark.github.io/pwa.rocks/',
+      finalUrl: 'https://connorjclark.github.io/pwa.rocks/',
       audits: {
         'is-on-https': {
           score: 1,
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 c074dd8d3..592692eb4 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unused-javascript-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unused-javascript-test.js
@@ -43,7 +43,7 @@ function generateRecord(url, transferSize, resourceType) {
   return {url, transferSize, resourceType};
 }
 
-function generateUsage(url, ranges, transferSize = 1000) {
+function generateUsage(url, ranges) {
   const functions = ranges.map(range => {
     return {
       ranges: [
@@ -56,134 +56,103 @@ function generateUsage(url, ranges, transferSize = 1000) {
     };
   });
 
-  return {url, functions, networkRecord: {transferSize}};
+  return {url, functions};
 }
 
-describe('UnusedJavaScript audit', () => {
-  describe('#computeWaste', () => {
-    it('should identify used', () => {
-      const usage = generateUsage('myscript.js', [[0, 100, true]]);
-      const result = UnusedJavaScript.computeWaste(usage);
-      assert.equal(result.unusedLength, 0);
-      assert.equal(result.contentLength, 100);
-    });
-
-    it('should identify unused', () => {
-      const usage = generateUsage('myscript.js', [[0, 100, false]]);
-      const result = UnusedJavaScript.computeWaste(usage);
-      assert.equal(result.unusedLength, 100);
-      assert.equal(result.contentLength, 100);
-    });
-
-    it('should identify nested unused', () => {
-      const usage = generateUsage('myscript.js', [
-        [0, 100, true], // 40% used overall
-
-        [0, 10, true],
-        [0, 40, true],
-        [20, 40, false],
-
-        [60, 100, false],
-        [70, 80, false],
-
-        [100, 150, false],
-        [180, 200, false],
-        [100, 200, true], // 30% used overall
-      ]);
+function makeJsUsage(...usages) {
+  return usages.reduce((acc, cur) => {
+    acc[cur.url] = acc[cur.url] || [];
+    acc[cur.url].push(cur);
+    return acc;
+  }, {});
+}
 
-      const result = UnusedJavaScript.computeWaste(usage);
-      assert.equal(result.unusedLength, 130);
-      assert.equal(result.contentLength, 200);
-    });
+describe('UnusedJavaScript audit', () => {
+  const domain = 'https://www.google.com';
+  const scriptUnknown = generateUsage(domain, [[0, 3000, false]]);
+  const scriptA = generateUsage(`${domain}/scriptA.js`, [[0, 100, true]]);
+  const scriptB = generateUsage(`${domain}/scriptB.js`, [[0, 200, true], [0, 50, false]]);
+  const inlineA = generateUsage(`${domain}/inline.html`, [[0, 5000, true], [5000, 6000, false]]);
+  const inlineB = generateUsage(`${domain}/inline.html`, [[0, 15000, true], [0, 5000, false]]);
+  const recordA = generateRecord(`${domain}/scriptA.js`, 35000, 'Script');
+  const recordB = generateRecord(`${domain}/scriptB.js`, 50000, 'Script');
+  const recordInline = generateRecord(`${domain}/inline.html`, 1000000, 'Document');
+
+  it('should merge duplicates', async () => {
+    const context = {computedCache: new Map()};
+    const networkRecords = [recordA, recordB, recordInline];
+    const artifacts = {
+      JsUsage: makeJsUsage(scriptA, scriptB, scriptUnknown, inlineA, inlineB),
+      devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
+      SourceMaps: [],
+    };
+    const result = await UnusedJavaScript.audit_(artifacts, networkRecords, context);
+    expect(result.items.map(item => item.url)).toEqual([
+      'https://www.google.com/scriptB.js',
+      'https://www.google.com/inline.html',
+    ]);
+
+    const scriptBWaste = result.items[0];
+    assert.equal(scriptBWaste.totalBytes, 50000);
+    assert.equal(scriptBWaste.wastedBytes, 12500);
+    assert.equal(scriptBWaste.wastedPercent, 25);
+
+    const inlineWaste = result.items[1];
+    assert.equal(inlineWaste.totalBytes, 21000);
+    assert.equal(inlineWaste.wastedBytes, 6000);
+    assert.equal(Math.round(inlineWaste.wastedPercent), 29);
   });
 
-  describe('audit_', () => {
-    const domain = 'https://www.google.com';
-    const scriptUnknown = generateUsage(domain, [[0, 3000, false]]);
-    const scriptA = generateUsage(`${domain}/scriptA.js`, [[0, 100, true]]);
-    const scriptB = generateUsage(`${domain}/scriptB.js`, [[0, 200, true], [0, 50, false]]);
-    const inlineA = generateUsage(`${domain}/inline.html`, [[0, 5000, true], [5000, 6000, false]]);
-    const inlineB = generateUsage(`${domain}/inline.html`, [[0, 15000, true], [0, 5000, false]]);
-    const recordA = generateRecord(`${domain}/scriptA.js`, 35000, 'Script');
-    const recordB = generateRecord(`${domain}/scriptB.js`, 50000, 'Script');
-    const recordInline = generateRecord(`${domain}/inline.html`, 1000000, 'Document');
-
-    it('should merge duplicates', async () => {
-      const context = {computedCache: new Map()};
-      const networkRecords = [recordA, recordB, recordInline];
-      const artifacts = {
-        JsUsage: [scriptA, scriptB, scriptUnknown, inlineA, inlineB],
-        devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
-      };
-      const result = await UnusedJavaScript.audit_(artifacts, networkRecords, context);
-      assert.equal(result.items.length, 2);
-
-      const scriptBWaste = result.items[0];
-      assert.equal(scriptBWaste.totalBytes, 50000);
-      assert.equal(scriptBWaste.wastedBytes, 12500);
-      assert.equal(scriptBWaste.wastedPercent, 25);
-
-      const inlineWaste = result.items[1];
-      assert.equal(inlineWaste.totalBytes, 21000);
-      assert.equal(inlineWaste.wastedBytes, 6000);
-      assert.equal(Math.round(inlineWaste.wastedPercent), 29);
-    });
-
-    it('should augment when provided source maps', async () => {
-      const context = {
-        computedCache: new Map(),
-        options: {
-          // Default threshold is 1024, but is lowered here so that squoosh actually generates
-          // results.
-          // TODO(cjamcl): the bundle visualization feature will require most of the logic currently
-          // done in unused-javascript to be moved to a computed artifact. When that happens, these
-          // tests will go there, and the artifact will not have any thresholds (filtering will happen
-          // within the audits), so this test will not need a threshold. Until then, it does.
-          bundleSourceUnusedThreshold: 100,
+  it('should augment when provided source maps', async () => {
+    const context = {
+      computedCache: new Map(),
+      options: {
+        // Default threshold is 512, but is lowered here so that squoosh generates more
+        // results.
+        bundleSourceUnusedThreshold: 100,
+      },
+    };
+    const {map, content, usage} = load('squoosh');
+    const url = 'https://squoosh.app/main-app.js';
+    const networkRecords = [generateRecord(url, content.length, 'Script')];
+    const artifacts = {
+      JsUsage: makeJsUsage(usage),
+      devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
+      SourceMaps: [{scriptUrl: url, map}],
+      ScriptElements: [{src: url, content}],
+    };
+    const result = await UnusedJavaScript.audit_(artifacts, networkRecords, context);
+
+    expect(result.items).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "sourceBytes": Array [
+            10062,
+            660,
+            4043,
+            2138,
+            4117,
+          ],
+          "sourceWastedBytes": Array [
+            3760,
+            660,
+            500,
+            293,
+            256,
+          ],
+          "sources": Array [
+            "(unmapped)",
+            "…src/codecs/webp/encoder-meta.ts",
+            "…src/lib/util.ts",
+            "…src/custom-els/RangeInput/index.ts",
+            "…node_modules/comlink/comlink.js",
+          ],
+          "totalBytes": 83748,
+          "url": "https://squoosh.app/main-app.js",
+          "wastedBytes": 6961,
+          "wastedPercent": 8.312435814764395,
         },
-      };
-      const {map, content, usage} = load('squoosh');
-      const url = 'https://squoosh.app/main-app.js';
-      const networkRecords = [generateRecord(url, content.length, 'Script')];
-      const artifacts = {
-        JsUsage: [usage],
-        devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
-        SourceMaps: [{scriptUrl: url, map}],
-        ScriptElements: [{src: url, content}],
-      };
-      const result = await UnusedJavaScript.audit_(artifacts, networkRecords, context);
-
-      expect(result.items).toMatchInlineSnapshot(`
-        Array [
-          Object {
-            "sourceBytes": Array [
-              10062,
-              660,
-              4043,
-              2138,
-              4117,
-            ],
-            "sourceWastedBytes": Array [
-              3760,
-              660,
-              500,
-              293,
-              256,
-            ],
-            "sources": Array [
-              "(unmapped)",
-              "…src/codecs/webp/encoder-meta.ts",
-              "…src/lib/util.ts",
-              "…src/custom-els/RangeInput/index.ts",
-              "…node_modules/comlink/comlink.js",
-            ],
-            "totalBytes": 83748,
-            "url": "https://squoosh.app/main-app.js",
-            "wastedBytes": 6961,
-            "wastedPercent": 8.312435814764395,
-          },
-        ]
-      `);
-    });
+      ]
+    `);
   });
 });
diff --git a/lighthouse-core/test/audits/legacy-javascript-test.js b/lighthouse-core/test/audits/legacy-javascript-test.js
index 4509e8544..4df65e4ac 100644
--- a/lighthouse-core/test/audits/legacy-javascript-test.js
+++ b/lighthouse-core/test/audits/legacy-javascript-test.js
@@ -10,7 +10,7 @@ const LegacyJavascript = require('../../audits/legacy-javascript.js');
 const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
 
 /**
- * @param {Array<{url: string, code: string}>} scripts
+ * @param {Array<{url: string, code: string, map?: LH.Artifacts.RawSourceMap}>} scripts
  * @return {LH.Artifacts}
  */
 const createArtifacts = (scripts) => {
@@ -21,13 +21,21 @@ const createArtifacts = (scripts) => {
   return {
     URL: {finalUrl: '', requestedUrl: ''},
     devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
-    ScriptElements: scripts.reduce((acc, {code}, index) => {
-      acc[String(index)] = {
+    ScriptElements: scripts.map(({url, code}, index) => {
+      return {
+        src: url,
         content: code,
         requestId: String(index),
       };
+    }),
+    SourceMaps: scripts.reduce((acc, {url, map}) => {
+      if (!map) return acc;
+      acc.push({
+        scriptUrl: url,
+        map,
+      });
       return acc;
-    }, {}),
+    }, []),
   };
 };
 
@@ -164,17 +172,53 @@ describe('LegacyJavaScript audit', () => {
     expect(result.details.items.map(item => getCodeForUrl(item.url))).toEqual([]);
     assert.equal(result.score, 1);
   });
+
+  it('uses source maps to identify polyfills', async () => {
+    const map = {
+      sources: [
+        'node_modules/blah/blah/es6.string.repeat.js',
+      ],
+      mappings: 'blah',
+    };
+    const script = {code: 'blah blah', url: 'https://www.example.com/0.js', map};
+    const artifacts = createArtifacts([script]);
+
+    const result = await LegacyJavascript.audit(artifacts, {computedCache: new Map()});
+    expect(result.details.items[0].signals).toEqual(['String.prototype.repeat']);
+    expect(result.details.items[0].locations).toMatchObject([{line: 0, column: 0}]);
+  });
+
+  it('uses location from pattern matching over source map', async () => {
+    const map = {
+      sources: [
+        'node_modules/blah/blah/es6.string.repeat.js',
+      ],
+      mappings: 'blah',
+    };
+    const script = {
+      code: 'some code;\nString.prototype.repeat = function() {}',
+      url: 'https://www.example.com/0.js',
+      map,
+    };
+    const artifacts = createArtifacts([script]);
+
+    const result = await LegacyJavascript.audit(artifacts, {computedCache: new Map()});
+    expect(result.details.items[0].signals).toEqual(['String.prototype.repeat']);
+    expect(result.details.items[0].locations).toMatchObject([{line: 1, column: 0}]);
+  });
 });
 
 describe('LegacyJavaScript signals', () => {
   it('expect babel-preset-env = true variant to not have any signals', () => {
-    const signalSummary = require('../../scripts/legacy-javascript/summary-signals.json');
-    const expectedMissingSignals = [
-      'core-js-2-preset-env-esmodules/true',
-      'core-js-3-preset-env-esmodules/true',
-    ];
-    for (const expectedVariant of expectedMissingSignals) {
-      expect(signalSummary.variantsMissingSignals).toContain(expectedVariant);
+    for (const summaryFilename of ['summary-signals.json', 'summary-signals-nomaps.json']) {
+      const signalSummary = require(`../../scripts/legacy-javascript/${summaryFilename}`);
+      const expectedMissingSignals = [
+        'core-js-2-preset-env-esmodules/true',
+        'core-js-3-preset-env-esmodules/true',
+      ];
+      for (const expectedVariant of expectedMissingSignals) {
+        expect(signalSummary.variantsMissingSignals).toContain(expectedVariant);
+      }
     }
   });
 });
diff --git a/lighthouse-core/test/audits/timing-budget-test.js b/lighthouse-core/test/audits/timing-budget-test.js
index 09917f593..94c3debe4 100644
--- a/lighthouse-core/test/audits/timing-budget-test.js
+++ b/lighthouse-core/test/audits/timing-budget-test.js
@@ -8,6 +8,8 @@
 const TimingBudgetAudit = require('../../audits/timing-budget.js');
 const trace = require('../fixtures/traces/progressive-app-m60.json');
 const devtoolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');
+const lcpTrace = require('../fixtures/traces/lcp-m78.json');
+const lcpDevtoolsLog = require('../fixtures/traces/lcp-m78.devtools.log.json');
 
 /* eslint-env jest */
 
@@ -80,30 +82,62 @@ describe('Performance: Timing budget audit', () => {
       expect(result.details.items).toHaveLength(2);
     });
 
-    it('works for all supported timing metrics', async () => {
-      const metrics = [
-        'first-contentful-paint',
-        'first-cpu-idle',
-        'interactive',
-        'first-meaningful-paint',
-        'max-potential-fid',
-        'estimated-input-latency',
-        'total-blocking-time',
-        'speed-index',
-      ];
-      await Promise.all(metrics.map(async (metric) => {
+    describe('timings metrics', () => {
+      it('work for all supported timing metrics', async () => {
+        const metrics = [
+          'first-contentful-paint',
+          'first-cpu-idle',
+          'interactive',
+          'first-meaningful-paint',
+          'max-potential-fid',
+          'estimated-input-latency',
+          'total-blocking-time',
+          'speed-index',
+          'largest-contentful-paint',
+          'cumulative-layout-shift',
+        ];
+        for (const metric of metrics) {
+          context.settings.budgets = [{
+            path: '/',
+            timings: [
+              {
+                metric: metric,
+                budget: 100,
+              },
+            ],
+          }];
+          const result = await TimingBudgetAudit.audit(artifacts, context);
+          expect(result.details.items[0].label).toBeDefined();
+          if (metric === 'largest-contentful-paint') {
+            expect(result.details.items[0].measurement).toEqual(undefined);
+          } else if (metric === 'cumulative-layout-shift') {
+            expect(result.details.items[0].measurement).toEqual(0);
+          } else {
+            expect(result.details.items[0].measurement).toBeGreaterThanOrEqual(1);
+          }
+        }
+      });
+
+      // Supplements test above. Uses a trace with a defined LCP for better test coverage.
+      it('supports Largest Contentful Paint', async () => {
+        artifacts.devtoolsLogs.defaultPass = lcpDevtoolsLog;
+        artifacts.traces.defaultPass = lcpTrace;
+
+        // Use an observed throttlingMethod so we don't have to worry about the value changing in the future.
+        context.settings.throttlingMethod = 'provided';
         context.settings.budgets = [{
           path: '/',
           timings: [
             {
-              metric: metric,
+              metric: 'largest-contentful-paint',
               budget: 100,
             },
           ],
         }];
         const result = await TimingBudgetAudit.audit(artifacts, context);
         expect(result.details.items).toHaveLength(1);
-      }));
+        expect(result.details.items[0].measurement).toEqual(1121.711);
+      });
     });
 
     it('sorts rows by descending budget overage', async () => {
diff --git a/lighthouse-core/test/computed/unused-javascript-summary-test.js b/lighthouse-core/test/computed/unused-javascript-summary-test.js
new file mode 100644
index 000000000..83087af9a
--- /dev/null
+++ b/lighthouse-core/test/computed/unused-javascript-summary-test.js
@@ -0,0 +1,64 @@
+/**
+ * @license Copyright 2020 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const assert = require('assert');
+const UnusedJavaScriptSummary = require('../../computed/unused-javascript-summary.js');
+
+/* eslint-env jest */
+
+function generateUsage(url, ranges) {
+  const functions = ranges.map(range => {
+    return {
+      ranges: [
+        {
+          startOffset: range[0],
+          endOffset: range[1],
+          count: range[2] ? 1 : 0,
+        },
+      ],
+    };
+  });
+
+  return {url, functions};
+}
+
+describe('UnusedJavaScriptSummary computed artifact', () => {
+  it('should identify used', () => {
+    const usage = generateUsage('myscript.js', [[0, 100, true]]);
+    const result = UnusedJavaScriptSummary.computeWaste(usage);
+    assert.equal(result.unusedLength, 0);
+    assert.equal(result.contentLength, 100);
+  });
+
+  it('should identify unused', () => {
+    const usage = generateUsage('myscript.js', [[0, 100, false]]);
+    const result = UnusedJavaScriptSummary.computeWaste(usage);
+    assert.equal(result.unusedLength, 100);
+    assert.equal(result.contentLength, 100);
+  });
+
+  it('should identify nested unused', () => {
+    const usage = generateUsage('myscript.js', [
+      [0, 100, true], // 40% used overall
+
+      [0, 10, true],
+      [0, 40, true],
+      [20, 40, false],
+
+      [60, 100, false],
+      [70, 80, false],
+
+      [100, 150, false],
+      [180, 200, false],
+      [100, 200, true], // 30% used overall
+    ]);
+
+    const result = UnusedJavaScriptSummary.computeWaste(usage);
+    assert.equal(result.unusedLength, 130);
+    assert.equal(result.contentLength, 200);
+  });
+});
diff --git a/lighthouse-core/test/config/budget-test.js b/lighthouse-core/test/config/budget-test.js
index 05a361b3c..b13eeef0b 100644
--- a/lighthouse-core/test/config/budget-test.js
+++ b/lighthouse-core/test/config/budget-test.js
@@ -233,6 +233,26 @@ describe('Budget', () => {
   });
 
   describe('timing budget validation', () => {
+    it('supports all timing metrics', () => {
+      budgets[0].timings = [{metric: 'blah', budget: 0}];
+      const metrics = [
+        'first-contentful-paint',
+        'first-cpu-idle',
+        'interactive',
+        'first-meaningful-paint',
+        'max-potential-fid',
+        'estimated-input-latency',
+        'total-blocking-time',
+        'speed-index',
+        'largest-contentful-paint',
+        'cumulative-layout-shift',
+      ];
+      for (const metric of metrics) {
+        budgets[0].timings[0].metric = metric;
+        const result = Budget.initializeBudget(budgets);
+        expect(result[0].timings[0].metric).toEqual(metric);
+      }
+    });
     it('throws when an invalid metric is supplied', () => {
       budgets[0].timings[0].metric = 'medianMeaningfulPaint';
       assert.throws(_ => Budget.initializeBudget(budgets),
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 32a0de28d..d5061e1b6 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -5730,7 +5730,7 @@
           "path": "audits[bootup-time].displayValue"
         }
       ],
-      "lighthouse-core/audits/metrics/largest-contentful-paint.js | title": [
+      "lighthouse-core/lib/i18n/i18n.js | largestContentfulPaintMetric": [
         "audits[largest-contentful-paint].title"
       ],
       "lighthouse-core/audits/metrics/largest-contentful-paint.js | description": [
@@ -5806,7 +5806,7 @@
       "lighthouse-core/audits/metrics/max-potential-fid.js | description": [
         "audits[max-potential-fid].description"
       ],
-      "lighthouse-core/audits/metrics/cumulative-layout-shift.js | title": [
+      "lighthouse-core/lib/i18n/i18n.js | cumulativeLayoutShiftMetric": [
         "audits[cumulative-layout-shift].title"
       ],
       "lighthouse-core/audits/metrics/cumulative-layout-shift.js | description": [