Repository: GoogleChrome/lighthouse
Pull request #12494 (2021-05-17 21:02:01)

core(script-treemap-data): default config
ref #11254

cc @brendankenny I also made a new Details type.

paulirish: love the rename btw
connorjclark: > I couldn't reproduce that typescript error locally

me neither. stumped.
connorjclark: it's the `nodes`, a property in both screenshot and treemap data ...
connorjclark: @brendankenny may have a workaround. otherwise, can use `treemapNodes`. sad.
brendankenny: I really can't reproduce this, even in a basically identical [branch](https://github.com/GoogleChrome/lighthouse/compare/tsctest?expand=1). The details types are fully discriminated by the required `type`, there's no reason they should need different property names otherwise. It would be nice to be able to repro this at all to see what's happening...
brendankenny: FWIW the error sounds similar to #12280, but that case was enabled by types with all-optional properties, which (again) shouldn't be possible with `LH.Audit.Details`.

Patch:
diff --git a/lighthouse-core/audits/script-treemap-data.js b/lighthouse-core/audits/script-treemap-data.js
index 8cfcc5442..63657c568 100644
--- a/lighthouse-core/audits/script-treemap-data.js
+++ b/lighthouse-core/audits/script-treemap-data.js
@@ -257,13 +257,12 @@ class ScriptTreemapDataAudit extends Audit {
    * @return {Promise<LH.Audit.Product>}
    */
   static async audit(artifacts, context) {
-    const treemapData = await ScriptTreemapDataAudit.makeNodes(artifacts, context);
+    const nodes = await ScriptTreemapDataAudit.makeNodes(artifacts, context);
 
-    // TODO: when out of experimental should make a new detail type.
-    /** @type {LH.Audit.Details.DebugData} */
+    /** @type {LH.Audit.Details.TreemapData} */
     const details = {
-      type: 'debugdata',
-      treemapData,
+      type: 'treemap-data',
+      nodes,
     };
 
     return {
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index d859948b7..271b67855 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -238,6 +238,7 @@ const defaultConfig = {
     'valid-source-maps',
     'preload-lcp-image',
     'full-page-screenshot',
+    'script-treemap-data',
     'manual/pwa-cross-browser',
     'manual/pwa-page-transitions',
     'manual/pwa-each-page-has-url',
diff --git a/lighthouse-core/config/experimental-config.js b/lighthouse-core/config/experimental-config.js
index efd98ab49..5d716d3e2 100644
--- a/lighthouse-core/config/experimental-config.js
+++ b/lighthouse-core/config/experimental-config.js
@@ -16,7 +16,6 @@ const config = {
   audits: [
     'autocomplete',
     'large-javascript-libraries',
-    'script-treemap-data',
     'csp-xss',
   ],
   categories: {
diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index d866f17bb..a65fa8702 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -70,6 +70,7 @@ class DetailsRenderer {
       case 'screenshot':
       case 'debugdata':
       case 'full-page-screenshot':
+      case 'treemap-data':
         return null;
 
       default: {
diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js
index 5658f81ab..3f5943114 100644
--- a/lighthouse-core/report/html/renderer/report-ui-features.js
+++ b/lighthouse-core/report/html/renderer/report-ui-features.js
@@ -183,7 +183,10 @@ class ReportUIFeatures {
    * @param {{text: string, icon?: string, onClick: () => void}} opts
    */
   addButton(opts) {
-    const metricsEl = this._dom.find('.lh-audit-group--metrics', this._document);
+    const metricsEl = this._document.querySelector('.lh-audit-group--metrics');
+    // Not supported without metrics group.
+    if (!metricsEl) return;
+
     const classes = [
       'lh-button',
     ];
@@ -546,9 +549,8 @@ class ReportUIFeatures {
    * @param {LH.Result} json
    */
   static openTreemap(json) {
-    const treemapDebugData = /** @type {LH.Audit.Details.DebugData} */ (
-      json.audits['script-treemap-data'].details);
-    if (!treemapDebugData) {
+    const treemapData = json.audits['script-treemap-data'].details;
+    if (!treemapData) {
       throw new Error('no script treemap data found');
     }
 
diff --git a/lighthouse-core/report/html/report-template.html b/lighthouse-core/report/html/report-template.html
index 74042a119..96c481103 100644
--- a/lighthouse-core/report/html/report-template.html
+++ b/lighthouse-core/report/html/report-template.html
@@ -48,11 +48,6 @@ limitations under the License.
       // is in the document.
       const features = new ReportUIFeatures(dom);
       features.initFeatures(window.__LIGHTHOUSE_JSON__);
-
-      // No UI for treemap yet. For now, must run this command in console.
-      globalThis._tmpFeatures = features;
-      const command = 'ReportUIFeatures.openTreemap(_tmpFeatures.json);';
-      console.log(`For treemap viewer, run: ${command}`);
     }
     window.addEventListener('DOMContentLoaded', __initLighthouseReport__);
 
diff --git a/lighthouse-treemap/app/debug.json b/lighthouse-treemap/app/debug.json
index a9421d8a8..9d5c3bd31 100644
--- a/lighthouse-treemap/app/debug.json
+++ b/lighthouse-treemap/app/debug.json
@@ -12077,8 +12077,8 @@
         "score": null,
         "scoreDisplayMode": "informative",
         "details": {
-          "type": "debugdata",
-          "treemapData": [
+          "type": "treemap-data",
+          "nodes": [
             {
               "name": "https://www.coursehero.com/",
               "resourceBytes": 69863
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js
index 5edfd5a8c..a9ec0a51a 100644
--- a/lighthouse-treemap/app/src/main.js
+++ b/lighthouse-treemap/app/src/main.js
@@ -9,8 +9,6 @@
 
 /* globals I18n webtreemap strings TreemapUtil Tabulator Cell Row */
 
-const UNUSED_BYTES_IGNORE_THRESHOLD = 20 * 1024;
-const UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_RATIO = 0.5;
 const DUPLICATED_MODULES_IGNORE_THRESHOLD = 1024;
 const DUPLICATED_MODULES_IGNORE_ROOT_RATIO = 0.01;
 
@@ -33,18 +31,14 @@ class TreemapViewer {
    * @param {HTMLElement} el
    */
   constructor(options, el) {
-    const treemapDebugData = /** @type {LH.Audit.Details.DebugData} */ (
-      options.lhr.audits['script-treemap-data'].details);
-    if (!treemapDebugData || !treemapDebugData.treemapData) {
+    const scriptTreemapData = options.lhr.audits['script-treemap-data'].details;
+    if (!scriptTreemapData || scriptTreemapData.type !== 'treemap-data') {
       throw new Error('missing script-treemap-data');
     }
 
-    /** @type {LH.Treemap.Node[]} */
-    const scriptNodes = treemapDebugData.treemapData;
-
     /** @type {{[group: string]: LH.Treemap.Node[]}} */
     this.depthOneNodesByGroup = {
-      scripts: scriptNodes,
+      scripts: scriptTreemapData.nodes,
     };
 
     /**
@@ -78,7 +72,7 @@ class TreemapViewer {
     this.viewModes;
     /** @type {RenderState=} */
     this.previousRenderState;
-    /** @type {WeakMap<HTMLElement, LH.Treemap.Node>} */
+    /** @type {WeakMap<HTMLElement, NodeWithElement>} */
     this.tableRowToNodeMap = new WeakMap();
     /** @type {WebTreeMap} */
     this.treemap;
@@ -161,10 +155,14 @@ class TreemapViewer {
       this.updateColors();
     });
 
-    treemapEl.addEventListener('keypress', (e) => {
+    treemapEl.addEventListener('keyup', (e) => {
       if (!(e instanceof KeyboardEvent)) return;
 
       if (e.key === 'Enter') this.updateColors();
+
+      if (e.key === 'Escape' && this.treemap) {
+        this.treemap.zoom([]); // zoom out to root
+      }
     });
 
     treemapEl.addEventListener('mouseover', (e) => {
@@ -224,29 +222,10 @@ class TreemapViewer {
     function createUnusedBytesViewMode(root) {
       if (root.unusedBytes === undefined) return;
 
-      /** @type {LH.Treemap.Highlight[]} */
-      const highlights = [];
-      for (const d1Node of root.children || []) {
-        // Only highlight leaf nodes if entire node (ie a JS bundle) has greater than a certain
-        // number of unused bytes.
-        if (!d1Node.unusedBytes || d1Node.unusedBytes < UNUSED_BYTES_IGNORE_THRESHOLD) continue;
-
-        TreemapUtil.walk(d1Node, (node, path) => {
-          // Only highlight leaf nodes of a certain ratio of unused bytes.
-          if (node.children) return;
-          if (!node.unusedBytes || !node.resourceBytes) return;
-          if (node.unusedBytes / node.resourceBytes < UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_RATIO) {
-            return;
-          }
-
-          highlights.push({path: [root.name, ...path]});
-        });
-      }
       return {
         id: 'unused-bytes',
         label: TreemapUtil.i18n.strings.unusedBytesLabel,
         subLabel: TreemapUtil.i18n.formatBytesWithBestUnit(root.unusedBytes),
-        highlights,
         enabled: true,
       };
     }
@@ -412,7 +391,7 @@ class TreemapViewer {
 
     if (rootChanged || viewChanged) {
       this.updateColors();
-      applyActiveClass(this.currentViewMode.id);
+      applyActiveClass(this.currentViewMode.id, this.el);
     }
 
     this.previousRenderState = {
@@ -425,7 +404,7 @@ class TreemapViewer {
     const tableEl = TreemapUtil.find('.lh-table');
     tableEl.innerHTML = '';
 
-    /** @type {Array<{node: LH.Treemap.Node, name: string, bundleNode?: LH.Treemap.Node, resourceBytes: number, unusedBytes?: number}>} */
+    /** @type {Array<{node: NodeWithElement, name: string, bundleNode?: LH.Treemap.Node, resourceBytes: number, unusedBytes?: number}>} */
     const data = [];
     TreemapUtil.walk(this.currentTreemapRoot, (node, path) => {
       if (node.children) return;
@@ -612,6 +591,8 @@ class TreemapViewer {
         this.getHueForD1NodeName(depthOneNode ? depthOneNode.name : node.name);
       const depthOneNodeColor = hue !== undefined ? this.getColorFromHue(hue) : 'white';
 
+      if (!node.dom) return;
+
       let backgroundColor;
       if (this.currentViewMode.highlights) {
         // A view can set nodes to highlight. If so, don't color anything else.
@@ -623,12 +604,16 @@ class TreemapViewer {
         } else {
           backgroundColor = 'white';
         }
-      } else {
-        backgroundColor = depthOneNodeColor;
+        node.dom.style.backgroundColor = backgroundColor;
+        return;
       }
 
-      if (node.dom) {
-        node.dom.style.backgroundColor = backgroundColor;
+      node.dom.style.backgroundColor = depthOneNodeColor;
+
+      // Shade the element to communicate coverage.
+      if (this.currentViewMode.id === 'unused-bytes') {
+        const pctUsed = (1 - (node.unusedBytes || 0) / node.resourceBytes) * 100;
+        node.dom.style.setProperty('--pctUsed', `${pctUsed}%`);
       }
     });
   }
@@ -673,14 +658,16 @@ function renderViewModeButtons(viewModes) {
 
 /**
  * @param {string} currentViewModeId
+ * @param {HTMLElement} el
  */
-function applyActiveClass(currentViewModeId) {
+function applyActiveClass(currentViewModeId, el) {
   const viewModesEl = TreemapUtil.find('.lh-modes');
   for (const viewModeEl of viewModesEl.querySelectorAll('.view-mode')) {
     if (!(viewModeEl instanceof HTMLElement)) continue;
 
-    viewModeEl.classList
-      .toggle('view-mode--active', viewModeEl.id === `view-mode--${currentViewModeId}`);
+    const isMatch = viewModeEl.id === `view-mode--${currentViewModeId}`;
+    viewModeEl.classList.toggle('view-mode--active', isMatch);
+    el.classList.toggle(`lh-treemap--${viewModeEl.id}`, isMatch);
   }
 }
 
diff --git a/lighthouse-treemap/app/src/util.js b/lighthouse-treemap/app/src/util.js
index e9c0d8260..c1d5c8ec5 100644
--- a/lighthouse-treemap/app/src/util.js
+++ b/lighthouse-treemap/app/src/util.js
@@ -14,7 +14,7 @@
 class TreemapUtil {
   /**
    * @param {LH.Treemap.Node} node
-   * @param {(node: LH.Treemap.Node, path: string[]) => void} fn
+   * @param {(node: NodeWithElement, path: string[]) => void} fn
    * @param {string[]=} path
    */
   static walk(node, fn, path) {
@@ -191,9 +191,7 @@ TreemapUtil.COLOR_HUES = [
   65.5,
   45,
   35.8,
-  14.4,
   15.9,
-  0,
   199.5,
 ];
 
diff --git a/lighthouse-treemap/app/styles/treemap.css b/lighthouse-treemap/app/styles/treemap.css
index e756e247f..4c1922e52 100644
--- a/lighthouse-treemap/app/styles/treemap.css
+++ b/lighthouse-treemap/app/styles/treemap.css
@@ -132,6 +132,13 @@ body {
   width: calc(100% * var(--unused) / var(--max));
   height: 7px;
   margin-left: 2px;
+  background: repeating-linear-gradient(
+          -45deg,
+          hsl(7, 85%, 58%),
+          hsl(7, 85%, 58%) 2px,
+          hsla(7, 80%, 45%, 1) 2px,
+          hsla(7, 80%, 45%, 1) 4px
+  );
 }
 
 .view-mode {
@@ -201,6 +208,23 @@ header {
   outline: 2px solid black;
 }
 
+.lh-treemap--view-mode--unused-bytes .webtreemap-node::before {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  right: 0;
+  content: '';
+  display: block;
+  width: var(--pctUsed);
+  background: repeating-linear-gradient(
+          -45deg,
+          hsla(0, 0%, 0%, 0),
+          hsla(0, 0%, 0%, 0) 2px,
+          hsla(7, 85%, 56%, 0.3) 2px,
+          hsla(7, 85%, 56%, 0.3) 4px
+  );
+}
+
 .webtreemap-caption {
   font-size: 12px;
   text-align: center;
diff --git a/lighthouse-treemap/types/treemap.d.ts b/lighthouse-treemap/types/treemap.d.ts
index f74fd5fa9..6043d599d 100644
--- a/lighthouse-treemap/types/treemap.d.ts
+++ b/lighthouse-treemap/types/treemap.d.ts
@@ -7,6 +7,7 @@ declare global {
     constructor(data: any, options: WebTreeMapOptions);
     render(el: HTMLElement): void;
     layout(data: any, el: HTMLElement): void;
+    zoom(address: number[]): void
   }
 
   interface WebTreeMapOptions {
@@ -21,6 +22,11 @@ declare global {
     viewMode: LH.Treemap.ViewMode;
   }
 
+  interface NodeWithElement extends LH.Treemap.Node {
+    /** webtreemap adds dom to node data. */
+    dom?: HTMLElement;
+  }
+
   var webtreemap: {
     TreeMap: typeof WebTreeMap;
     render(el: HTMLElement, data: any, options: WebTreeMapOptions): void;
diff --git a/types/audit-details.d.ts b/types/audit-details.d.ts
index b982b7fc0..6eab46c37 100644
--- a/types/audit-details.d.ts
+++ b/types/audit-details.d.ts
@@ -9,6 +9,7 @@ declare global {
     export type Details =
       Details.CriticalRequestChain |
       Details.DebugData |
+      Details.TreemapData |
       Details.Filmstrip |
       Details.List |
       Details.Opportunity |
@@ -97,6 +98,11 @@ declare global {
         [p: string]: any;
       }
 
+      export interface TreemapData {
+        type: 'treemap-data';
+        nodes: LH.Treemap.Node[];
+      }
+
       /** String enum of possible types of values found within table items. */
       type ItemValueType = 'bytes' | 'code' | 'link' | 'ms' | 'multi' | 'node' | 'source-location' | 'numeric' | 'text' | 'thumbnail' | 'timespanMs' | 'url';
 
diff --git a/types/audit.d.ts b/types/audit.d.ts
index 131009067..687169392 100644
--- a/types/audit.d.ts
+++ b/types/audit.d.ts
@@ -134,7 +134,7 @@ declare global {
       /** The unit of `numericValue`, used when the consumer wishes to convert numericValue to a display string. */
       numericUnit?: string;
       /** Extra information about the page provided by some types of audits, in one of several possible forms that can be rendered in the HTML report. */
-      details?: FormattedIcu<Audit.Details>;
+      details?: FormattedIcu<LH.Audit.Details>;
     }
 
     export interface Results {
diff --git a/types/treemap.d.ts b/types/treemap.d.ts
index 74c968fab..e1ec26251 100644
--- a/types/treemap.d.ts
+++ b/types/treemap.d.ts
@@ -40,8 +40,6 @@ declare global {
       /** If present, this module is a duplicate. String is normalized source path. See ModuleDuplication.normalizeSource */
       duplicatedNormalizedModuleName?: string;
       children?: Node[];
-      /** Added by webtreemap lib. */
-      dom?: HTMLElement;
     }
   }
 }


Test patch:
diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index cc9bc18f3..7847f74e5 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -171,6 +171,9 @@ Object {
     Object {
       "path": "full-page-screenshot",
     },
+    Object {
+      "path": "script-treemap-data",
+    },
     Object {
       "path": "manual/pwa-cross-browser",
     },
diff --git a/lighthouse-core/test/audits/script-treemap-data-test.js b/lighthouse-core/test/audits/script-treemap-data-test.js
index 1e5ee8344..70d05c618 100644
--- a/lighthouse-core/test/audits/script-treemap-data-test.js
+++ b/lighthouse-core/test/audits/script-treemap-data-test.js
@@ -28,7 +28,7 @@ function generateRecord(url, resourceSize, resourceType) {
 
 describe('ScriptTreemapData audit', () => {
   describe('squoosh fixture', () => {
-    /** @type {LH.Treemap.Node[]} */
+    /** @type {LH.Audit.Details.TreemapData} */
     let treemapData;
     beforeAll(async () => {
       const context = {computedCache: new Map()};
@@ -52,13 +52,15 @@ describe('ScriptTreemapData audit', () => {
         ScriptElements: [{src: scriptUrl, content}, noSourceMapScript],
       };
       const results = await ScriptTreemapData.audit(artifacts, context);
+      if (!results.details || results.details.type !== 'treemap-data') {
+        throw new Error('should not happen.');
+      }
 
-      // @ts-expect-error: Debug data.
-      treemapData = results.details.treemapData;
+      treemapData = results.details;
     });
 
     it('has nodes', () => {
-      expect(treemapData.find(s => s.name === 'https://sqoosh.app/no-map-or-usage.js'))
+      expect(treemapData.nodes.find(s => s.name === 'https://sqoosh.app/no-map-or-usage.js'))
         .toMatchInlineSnapshot(`
         Object {
           "name": "https://sqoosh.app/no-map-or-usage.js",
@@ -66,7 +68,7 @@ describe('ScriptTreemapData audit', () => {
         }
       `);
 
-      const bundleNode = treemapData.find(s => s.name === 'https://squoosh.app/main-app.js');
+      const bundleNode = treemapData.nodes.find(s => s.name === 'https://squoosh.app/main-app.js');
       // @ts-expect-error
       const unmapped = bundleNode.children[0].children.find(m => m.name === '(unmapped)');
       expect(unmapped).toMatchInlineSnapshot(`
@@ -77,13 +79,13 @@ describe('ScriptTreemapData audit', () => {
         }
       `);
 
-      expect(JSON.stringify(treemapData).length).toMatchInlineSnapshot(`6740`);
-      expect(treemapData).toMatchSnapshot();
+      expect(JSON.stringify(treemapData.nodes).length).toMatchInlineSnapshot(`6740`);
+      expect(treemapData.nodes).toMatchSnapshot();
     });
   });
 
   describe('coursehero fixture', () => {
-    /** @type {LH.Treemap.Node[]} */
+    /** @type {LH.Audit.Details.TreemapData} */
     let treemapData;
     beforeAll(async () => {
       const context = {computedCache: new Map()};
@@ -106,20 +108,22 @@ describe('ScriptTreemapData audit', () => {
         ScriptElements: [{src: scriptUrl1, content}, {src: scriptUrl2, content}],
       };
       const results = await ScriptTreemapData.audit(artifacts, context);
+      if (!results.details || results.details.type !== 'treemap-data') {
+        throw new Error('should not happen.');
+      }
 
-      // @ts-expect-error: Debug data.
-      treemapData = results.details.treemapData;
+      treemapData = results.details;
     });
 
     it('has nodes', () => {
-      expect(JSON.stringify(treemapData).length).toMatchInlineSnapshot(`86817`);
-      expect(treemapData).toMatchSnapshot();
+      expect(JSON.stringify(treemapData.nodes).length).toMatchInlineSnapshot(`86817`);
+      expect(treemapData.nodes).toMatchSnapshot();
     });
 
     it('finds duplicates', () => {
-      expect(JSON.stringify(treemapData).length).toMatchInlineSnapshot(`86817`);
+      expect(JSON.stringify(treemapData.nodes).length).toMatchInlineSnapshot(`86817`);
       // @ts-ignore all these children exist.
-      const leafNode = treemapData[0].
+      const leafNode = treemapData.nodes[0].
         children[0].
         children[0].
         children[0].
diff --git a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
index d08a53861..276690bc2 100644
--- a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
@@ -556,11 +556,13 @@ describe('ReportUIFeatures', () => {
 
   describe('treemap button', () => {
     it('should only show button if treemap data is available', () => {
-      expect(sampleResults.audits['script-treemap-data']).toBeUndefined();
-      expect(render(sampleResults).querySelector('.lh-button.report-icon--treemap')).toBeNull();
+      const lhr = JSON.parse(JSON.stringify(sampleResults));
+
+      expect(lhr.audits['script-treemap-data']).not.toBeUndefined();
+      expect(render(lhr).querySelector('.lh-button.report-icon--treemap')).toBeTruthy();
 
-      sampleResults.audits['script-treemap-data'] = {details: {}};
-      expect(render(sampleResults).querySelector('.lh-button.report-icon--treemap')).toBeTruthy();
+      delete lhr.audits['script-treemap-data'];
+      expect(render(lhr).querySelector('.lh-button.report-icon--treemap')).toBeNull();
     });
   });
 
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index aab9cd781..b2003db5e 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -2505,6 +2505,42 @@
         }
       }
     },
+    "script-treemap-data": {
+      "id": "script-treemap-data",
+      "title": "Script Treemap Data",
+      "description": "Used for treemap app",
+      "score": null,
+      "scoreDisplayMode": "informative",
+      "details": {
+        "type": "treemap-data",
+        "nodes": [
+          {
+            "name": "http://localhost:10200/dobetterweb/dbw_tester.html",
+            "resourceBytes": 5806
+          },
+          {
+            "name": "http://localhost:10200/dobetterweb/dbw_tester.js",
+            "resourceBytes": 48
+          },
+          {
+            "name": "http://localhost:10200/dobetterweb/empty_module.js?delay=500",
+            "resourceBytes": 60
+          },
+          {
+            "name": "http://localhost:10200/dobetterweb/fcp-delayer.js?delay=5000",
+            "resourceBytes": 60
+          },
+          {
+            "name": "http://localhost:10200/zone.js",
+            "resourceBytes": 30
+          },
+          {
+            "name": "http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js",
+            "resourceBytes": 63
+          }
+        ]
+      }
+    },
     "pwa-cross-browser": {
       "id": "pwa-cross-browser",
       "title": "Site works cross-browser",
@@ -6396,6 +6432,24 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:audit:script-treemap-data",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:JSBundles",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:ModuleDuplication",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:pwa-cross-browser",