Repository: GoogleChrome/lighthouse
Pull request #7030 (2019-01-16 15:57:41)

core(images): cleanup ImageUsage to match other *Elements artifacts
**Summary**
Cleans up the `ImageUsage` artifact to remove unnecessary network record and align with other `*Elements` artifacts.

**Related Issues/PRs**
#6747 



Patch:
diff --git a/build/build-extension.js b/build/build-extension.js
index cf7e9d1bd..9c1085fc9 100644
--- a/build/build-extension.js
+++ b/build/build-extension.js
@@ -78,7 +78,7 @@ async function packageExtension() {
     writeStream.on('error', reject);
 
     archive.pipe(writeStream);
-    archive.glob(`${distDir}/**`);
+    archive.directory(distDir, false);
     archive.finalize();
   });
 }
diff --git a/changelog.md b/changelog.md
index bd7cc4c6e..69365365e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,11 +1,11 @@
  <a name="4.0.0"></a>
 # 4.0.0 (2019-01-16)
 
-## New Contributors!
+## New contributors!
 
 @mattzeunert, @dima74, @jeffbcross, @knoxmic, and @shogunsea. Thanks!
 
-## Major Changes
+## Major changes
 
 * A bevy of bug fixes and performance improvements.
 
@@ -15,6 +15,23 @@
 
 <a href="https://user-images.githubusercontent.com/316891/51218948-6e60ef00-18e3-11e9-8a48-535b59a41301.png"><img height="300" alt="Lighthouse 4.0 PWA category" src="https://user-images.githubusercontent.com/316891/51218948-6e60ef00-18e3-11e9-8a48-535b59a41301.png"></a>
 
+## Breaking changes
+
+* New PWA category, organization, and scoring ([#6395](https://github.com/GoogleChrome/lighthouse/issues/6395))
+  - the PWA section of the report has a set of badges instead of a numeric score gauge ([#6526](https://github.com/googlechrome/lighthouse/pull/6526), [#6670](https://github.com/googlechrome/lighthouse/pull/6670))
+  - the `webapp-install-banner` audit is now `installable-manifest` ([#6630](https://github.com/googlechrome/lighthouse/pull/6630))
+  - the offline check formerly in `webapp-install-banner` is the new audit `offline-start-url` ([#6397](https://github.com/googlechrome/lighthouse/pull/6397))
+* audits' `scoreDisplayMode` `'not-applicable'` is now `'notApplicable'` ([#6783](https://github.com/googlechrome/lighthouse/pull/6783))
+* `no-websql` audit removed due to performance cost ([#6293](https://github.com/googlechrome/lighthouse/pull/6293))
+* `speed-index` scoring now scales based on throttling ([#7007](https://github.com/googlechrome/lighthouse/pull/7007))
+* empty children arrays are now removed from `critical-request-chain` audit result ([#6211](https://github.com/googlechrome/lighthouse/pull/6211))
+* the correct Nexus 5X screen height of 660 now used instead of 732 ([#6932](https://github.com/googlechrome/lighthouse/pull/6932))
+* throttling constants under `mobile3G` renamed to the more accurate `mobileSlow4G` with no change in values ([#6163](https://github.com/googlechrome/lighthouse/pull/6163))
+* typescript definition files are now located under `types/` ([#6617](https://github.com/googlechrome/lighthouse/pull/6617))
+* computed artifact files are now located under `lighthouse-core/computed/` ([#6618](https://github.com/googlechrome/lighthouse/pull/6618))
+
+<hr>
+
 [Full Changelog](https://github.com/googlechrome/lighthouse/compare/v4.0.0-beta...v4.0.0) (in addition to the changes in [4.0.0-beta](https://github.com/GoogleChrome/lighthouse/releases/tag/v4.0.0-beta), [4.0.0-alpha.1](https://github.com/GoogleChrome/lighthouse/releases/tag/v4.0.0-alpha.1), and [4.0.0-alpha.0](https://github.com/GoogleChrome/lighthouse/releases/tag/4.0.0-alpha.0))
 
 ## Core
diff --git a/lighthouse-core/audits/byte-efficiency/offscreen-images.js b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
index 305e0a558..79472b92d 100644
--- a/lighthouse-core/audits/byte-efficiency/offscreen-images.js
+++ b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
@@ -47,7 +47,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['ImageUsage', 'ViewportDimensions', 'devtoolsLogs', 'traces'],
+      requiredArtifacts: ['ImageElements', 'ViewportDimensions', 'devtoolsLogs', 'traces'],
     };
   }
 
@@ -69,21 +69,23 @@ class OffscreenImages extends ByteEfficiencyAudit {
   }
 
   /**
-   * @param {LH.Artifacts.SingleImageUsage} image
+   * @param {LH.Artifacts.ImageElement} image
    * @param {{innerWidth: number, innerHeight: number}} viewportDimensions
+   * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
    * @return {null|Error|WasteResult}
    */
-  static computeWaste(image, viewportDimensions) {
-    if (!image.networkRecord) {
+  static computeWaste(image, viewportDimensions, networkRecords) {
+    const networkRecord = networkRecords.find(record => record.url === image.src);
+    if (!image.resourceSize || !networkRecord) {
       return null;
     }
 
     const url = URL.elideDataURI(image.src);
-    const totalPixels = image.clientWidth * image.clientHeight;
+    const totalPixels = image.displayedWidth * image.displayedHeight;
     const visiblePixels = this.computeVisiblePixels(image.clientRect, viewportDimensions);
     // Treat images with 0 area as if they're offscreen. See https://github.com/GoogleChrome/lighthouse/issues/1914
     const wastedRatio = totalPixels === 0 ? 1 : 1 - visiblePixels / totalPixels;
-    const totalBytes = image.networkRecord.resourceSize;
+    const totalBytes = image.resourceSize;
     const wastedBytes = Math.round(totalBytes * wastedRatio);
 
     if (!Number.isFinite(wastedRatio)) {
@@ -92,7 +94,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
 
     return {
       url,
-      requestStartTime: image.networkRecord.startTime,
+      requestStartTime: networkRecord.startTime,
       totalBytes,
       wastedBytes,
       wastedPercent: 100 * wastedRatio,
@@ -168,7 +170,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
    * @return {Promise<ByteEfficiencyAudit.ByteEfficiencyProduct>}
    */
   static async audit_(artifacts, networkRecords, context) {
-    const images = artifacts.ImageUsage;
+    const images = artifacts.ImageElements;
     const viewportDimensions = artifacts.ViewportDimensions;
     const trace = artifacts.traces[ByteEfficiencyAudit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[ByteEfficiencyAudit.DEFAULT_PASS];
@@ -176,7 +178,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
     /** @type {string[]} */
     const warnings = [];
     const resultsMap = images.reduce((results, image) => {
-      const processed = OffscreenImages.computeWaste(image, viewportDimensions);
+      const processed = OffscreenImages.computeWaste(image, viewportDimensions, networkRecords);
       if (processed === null) {
         return results;
       }
diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
index bd2293524..9769ca6ca 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
@@ -42,26 +42,26 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['ImageUsage', 'ViewportDimensions', 'devtoolsLogs', 'traces'],
+      requiredArtifacts: ['ImageElements', 'ViewportDimensions', 'devtoolsLogs', 'traces'],
     };
   }
 
   /**
-   * @param {LH.Artifacts.SingleImageUsage} image
+   * @param {LH.Artifacts.ImageElement} image
    * @param {number} DPR devicePixelRatio
    * @return {null|Error|LH.Audit.ByteEfficiencyItem};
    */
   static computeWaste(image, DPR) {
     // Nothing can be done without network info.
-    if (!image.networkRecord) {
+    if (!image.resourceSize) {
       return null;
     }
 
     const url = URL.elideDataURI(image.src);
     const actualPixels = image.naturalWidth * image.naturalHeight;
-    const usedPixels = image.clientWidth * image.clientHeight * Math.pow(DPR, 2);
+    const usedPixels = image.displayedWidth * image.displayedHeight * Math.pow(DPR, 2);
     const wastedRatio = 1 - (usedPixels / actualPixels);
-    const totalBytes = image.networkRecord.resourceSize;
+    const totalBytes = image.resourceSize;
     const wastedBytes = Math.round(totalBytes * wastedRatio);
 
     // If the image has 0 dimensions, it's probably hidden/offscreen, so let the offscreen-images
@@ -87,7 +87,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
    * @return {ByteEfficiencyAudit.ByteEfficiencyProduct}
    */
   static audit_(artifacts) {
-    const images = artifacts.ImageUsage;
+    const images = artifacts.ImageElements;
     const DPR = artifacts.ViewportDimensions.devicePixelRatio;
 
     /** @type {string[]} */
@@ -96,7 +96,7 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
     const resultsMap = new Map();
     images.forEach(image => {
       // TODO: give SVG a free pass until a detail per pixel metric is available
-      if (!image.networkRecord || image.networkRecord.mimeType === 'image/svg+xml') {
+      if (!image.resourceSize || image.mimeType === 'image/svg+xml') {
         return;
       }
 
diff --git a/lighthouse-core/audits/image-aspect-ratio.js b/lighthouse-core/audits/image-aspect-ratio.js
index 750acafff..37dafc981 100644
--- a/lighthouse-core/audits/image-aspect-ratio.js
+++ b/lighthouse-core/audits/image-aspect-ratio.js
@@ -16,7 +16,7 @@ const Audit = require('./audit');
 const URL = require('../lib/url-shim');
 const THRESHOLD_PX = 2;
 
-/** @typedef {Required<LH.Artifacts.SingleImageUsage>} WellDefinedImage */
+/** @typedef {Required<LH.Artifacts.ImageElement>} WellDefinedImage */
 
 class ImageAspectRatio extends Audit {
   /**
@@ -29,7 +29,7 @@ class ImageAspectRatio extends Audit {
       failureTitle: 'Displays images with incorrect aspect ratio',
       description: 'Image display dimensions should match natural aspect ratio. ' +
         '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/aspect-ratio).',
-      requiredArtifacts: ['ImageUsage'],
+      requiredArtifacts: ['ImageElements'],
     };
   }
 
@@ -40,10 +40,10 @@ class ImageAspectRatio extends Audit {
   static computeAspectRatios(image) {
     const url = URL.elideDataURI(image.src);
     const actualAspectRatio = image.naturalWidth / image.naturalHeight;
-    const displayedAspectRatio = image.width / image.height;
+    const displayedAspectRatio = image.displayedWidth / image.displayedHeight;
 
-    const targetDisplayHeight = image.width / actualAspectRatio;
-    const doRatiosMatch = Math.abs(targetDisplayHeight - image.height) < THRESHOLD_PX;
+    const targetDisplayHeight = image.displayedWidth / actualAspectRatio;
+    const doRatiosMatch = Math.abs(targetDisplayHeight - image.displayedHeight) < THRESHOLD_PX;
 
     if (!Number.isFinite(actualAspectRatio) ||
       !Number.isFinite(displayedAspectRatio)) {
@@ -52,7 +52,7 @@ class ImageAspectRatio extends Audit {
 
     return {
       url,
-      displayedAspectRatio: `${image.width} x ${image.height}
+      displayedAspectRatio: `${image.displayedWidth} x ${image.displayedHeight}
         (${displayedAspectRatio.toFixed(2)})`,
       actualAspectRatio: `${image.naturalWidth} x ${image.naturalHeight}
         (${actualAspectRatio.toFixed(2)})`,
@@ -65,7 +65,7 @@ class ImageAspectRatio extends Audit {
    * @return {LH.Audit.Product}
    */
   static audit(artifacts) {
-    const images = artifacts.ImageUsage;
+    const images = artifacts.ImageElements;
 
     /** @type {string[]} */
     const warnings = [];
@@ -75,12 +75,12 @@ class ImageAspectRatio extends Audit {
       // - filter out images that don't have following properties:
       //   networkRecord, width, height, images that use `object-fit`: `cover` or `contain`
       // - filter all svgs as they have no natural dimensions to audit
-      return image.networkRecord &&
-        image.networkRecord.mimeType !== 'image/svg+xml' &&
+      return image.mimeType &&
+        image.mimeType !== 'image/svg+xml' &&
         image.naturalHeight > 5 &&
         image.naturalWidth > 5 &&
-        image.width &&
-        image.height &&
+        image.displayedWidth &&
+        image.displayedHeight &&
         !image.usesObjectFit;
     }).forEach(image => {
       const wellDefinedImage = /** @type {WellDefinedImage} */ (image);
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 75df84c46..29aaaff75 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -92,8 +92,8 @@ const defaultConfig = {
       'manifest',
       'runtime-exceptions',
       'chrome-console-messages',
-      'image-usage',
       'accessibility',
+      'image-elements',
       'link-elements',
       'dobetterweb/anchors-with-no-rel-noopener',
       'dobetterweb/appcache',
diff --git a/lighthouse-core/gather/gatherers/image-usage.js b/lighthouse-core/gather/gatherers/image-elements.js
similarity index 78%
rename from lighthouse-core/gather/gatherers/image-usage.js
rename to lighthouse-core/gather/gatherers/image-elements.js
index 6071b86af..ebb01908b 100644
--- a/lighthouse-core/gather/gatherers/image-usage.js
+++ b/lighthouse-core/gather/gatherers/image-elements.js
@@ -15,7 +15,7 @@ const Driver = require('../driver.js'); // eslint-disable-line no-unused-vars
 
 /* global window, getElementsInDocument, Image */
 
-/** @return {Array<LH.Artifacts.SingleImageUsage>} */
+/** @return {Array<LH.Artifacts.ImageElement>} */
 /* istanbul ignore next */
 function collectImageElementInfo() {
   /** @param {Element} element */
@@ -37,21 +37,20 @@ function collectImageElementInfo() {
     return element.localName === 'img';
   }));
 
-  /** @type {Array<LH.Artifacts.SingleImageUsage>} */
+  /** @type {Array<LH.Artifacts.ImageElement>} */
   const htmlImages = allImageElements.map(element => {
     const computedStyle = window.getComputedStyle(element);
     return {
       // currentSrc used over src to get the url as determined by the browser
       // after taking into account srcset/media/sizes/etc.
       src: element.currentSrc,
-      width: element.width,
-      height: element.height,
-      clientWidth: element.clientWidth,
-      clientHeight: element.clientHeight,
+      displayedWidth: element.width,
+      displayedHeight: element.height,
       clientRect: getClientRect(element),
       naturalWidth: element.naturalWidth,
       naturalHeight: element.naturalHeight,
       isCss: false,
+      resourceSize: 0, // this will get overwritten below
       isPicture: !!element.parentElement && element.parentElement.tagName === 'PICTURE',
       usesObjectFit: ['cover', 'contain', 'scale-down', 'none'].includes(
         computedStyle.getPropertyValue('object-fit')
@@ -84,8 +83,8 @@ function collectImageElementInfo() {
 
     images.push({
       src: url,
-      clientWidth: element.clientWidth,
-      clientHeight: element.clientHeight,
+      displayedWidth: element.clientWidth,
+      displayedHeight: element.clientHeight,
       clientRect: getClientRect(element),
       // CSS Images do not expose natural size, we'll determine the size later
       naturalWidth: Number.MAX_VALUE,
@@ -93,10 +92,11 @@ function collectImageElementInfo() {
       isCss: true,
       isPicture: false,
       usesObjectFit: false,
+      resourceSize: 0, // this will get overwritten below
     });
 
     return images;
-  }, /** @type {Array<LH.Artifacts.SingleImageUsage>} */ ([]));
+  }, /** @type {Array<LH.Artifacts.ImageElement>} */ ([]));
 
   return htmlImages.concat(cssImages);
 }
@@ -121,11 +121,11 @@ function determineNaturalSize(url) {
   });
 }
 
-class ImageUsage extends Gatherer {
+class ImageElements extends Gatherer {
   /**
    * @param {Driver} driver
-   * @param {LH.Artifacts.SingleImageUsage} element
-   * @return {Promise<LH.Artifacts.SingleImageUsage>}
+   * @param {LH.Artifacts.ImageElement} element
+   * @return {Promise<LH.Artifacts.ImageElement>}
    */
   async fetchElementWithSizeInformation(driver, element) {
     const url = JSON.stringify(element.src);
@@ -142,42 +142,43 @@ class ImageUsage extends Gatherer {
   /**
    * @param {LH.Gatherer.PassContext} passContext
    * @param {LH.Gatherer.LoadData} loadData
-   * @return {Promise<LH.Artifacts['ImageUsage']>}
+   * @return {Promise<LH.Artifacts['ImageElements']>}
    */
   async afterPass(passContext, loadData) {
     const driver = passContext.driver;
     const indexedNetworkRecords = loadData.networkRecords.reduce((map, record) => {
       if (/^image/.test(record.mimeType) && record.finished) {
-        map[record.url] = {
-          url: record.url,
-          resourceSize: Math.min(record.resourceSize || 0, record.transferSize),
-          startTime: record.startTime,
-          endTime: record.endTime,
-          responseReceivedTime: record.responseReceivedTime,
-          mimeType: record.mimeType,
-        };
+        map[record.url] = record;
       }
 
       return map;
-    }, /** @type {Object<string, LH.Artifacts.SingleImageUsage['networkRecord']>} */ ({}));
+    }, /** @type {Object<string, LH.Artifacts.NetworkRequest>} */ ({}));
 
     const expression = `(function() {
       ${pageFunctions.getElementsInDocumentString}; // define function on page
       return (${collectImageElementInfo.toString()})();
     })()`;
 
-    /** @type {Array<LH.Artifacts.SingleImageUsage>} */
+    /** @type {Array<LH.Artifacts.ImageElement>} */
     const elements = await driver.evaluateAsync(expression);
 
     const imageUsage = [];
     for (let element of elements) {
-      // link up the image with its network record
-      element.networkRecord = indexedNetworkRecords[element.src];
+      // Pull some of our information directly off the network record.
+      const networkRecord = indexedNetworkRecords[element.src] || {};
+      element.mimeType = networkRecord.mimeType;
+      // Resource size is almost always the right one to be using because of the below:
+      //     transferSize = resourceSize + headers.length
+      // HOWEVER, there are some cases where an image is compressed again over the network and transfer size
+      // is smaller (see https://github.com/GoogleChrome/lighthouse/pull/4968).
+      // Use the min of the two numbers to be safe.
+      const {resourceSize = 0, transferSize = 0} = networkRecord;
+      element.resourceSize = Math.min(resourceSize, transferSize);
 
       // Images within `picture` behave strangely and natural size information isn't accurate,
       // CSS images have no natural size information at all. Try to get the actual size if we can.
       // Additional fetch is expensive; don't bother if we don't have a networkRecord for the image.
-      if ((element.isPicture || element.isCss) && element.networkRecord) {
+      if ((element.isPicture || element.isCss) && networkRecord) {
         element = await this.fetchElementWithSizeInformation(driver, element);
       }
 
@@ -188,4 +189,4 @@ class ImageUsage extends Gatherer {
   }
 }
 
-module.exports = ImageUsage;
+module.exports = ImageElements;
diff --git a/lighthouse-core/gather/gatherers/seo/font-size.js b/lighthouse-core/gather/gatherers/seo/font-size.js
index 5fe2bdeba..76493ef66 100644
--- a/lighthouse-core/gather/gatherers/seo/font-size.js
+++ b/lighthouse-core/gather/gatherers/seo/font-size.js
@@ -182,7 +182,8 @@ function getEffectiveFontRule({inlineStyle, matchedCSSRules, inherited}) {
  * @returns {number}
  */
 function getNodeTextLength(node) {
-  return !node.nodeValue ? 0 : node.nodeValue.trim().length;
+  // Array.from to count symbols not unicode code points. See: #6973
+  return !node.nodeValue ? 0 : Array.from(node.nodeValue.trim()).length;
 }
 
 /**
diff --git a/lighthouse-viewer/app/src/lighthouse-report-viewer.js b/lighthouse-viewer/app/src/lighthouse-report-viewer.js
index c8a76a158..f0464dfd5 100644
--- a/lighthouse-viewer/app/src/lighthouse-report-viewer.js
+++ b/lighthouse-viewer/app/src/lighthouse-report-viewer.js
@@ -138,13 +138,12 @@ class LighthouseReportViewer {
   _replaceReportHtml(json) {
     this._validateReportJson(json);
 
+    // Redirect to old viewer if a v2 report. v3 and v4 both handled by v4 viewer.
     if (json.lighthouseVersion.startsWith('2')) {
       this._loadInLegacyViewerVersion(json);
       return;
     }
 
-    // TODO: viewer3x :)
-
     const dom = new DOM(document);
     const renderer = new ReportRenderer(dom);
 
@@ -202,7 +201,7 @@ class LighthouseReportViewer {
   }
 
   /**
-   * Stores v2.x report in IDB, then navigates to legacy viewer in current tab
+   * Stores v2.x report in IDB, then navigates to legacy viewer in current tab.
    * @param {LH.ReportResult} reportJson
    * @private
    */
@@ -210,6 +209,7 @@ class LighthouseReportViewer {
     const warnMsg = `Version mismatch between viewer and JSON. Opening compatible viewer...`;
     logger.log(warnMsg, false);
 
+    // TODO: Handle 4x reports if we break viewer compat moving to v5.
     // Place report in IDB, then navigate current tab to the legacy viewer
     const viewerPath = new URL('../viewer2x/', location.href);
     idbKeyval.set('2xreport', reportJson).then(_ => {
diff --git a/readme.md b/readme.md
index d0cc15dc9..7ffe97c9a 100644
--- a/readme.md
+++ b/readme.md
@@ -267,6 +267,8 @@ This section details services that have integrated Lighthouse data. If you're wo
 
 * **[Calibre](https://calibreapp.com)** - Calibre is a web performance monitoring tool running Lighthouse continuously or on-demand via an API. Test using emulated devices and connection speeds from a number of geographical locations. Set budgets and improve performance with actionable guidelines. Calibre comes with a free 14-day trial.
 
+* **[DebugBear](https://www.debugbear.com/)** - DebugBear is a website monitoring tool based on Lighthouse. See how your scores and metrics changed over time, with a focus on understanding what caused each change. DebugBear is a paid product with a free 30-day trial.
+
 * **[HTTPArchive](http://httparchive.org/)** - HTTPArchive tracks how the web is built by crawling 500k pages with Web Page Test, including Lighthouse results, and stores the information in BigQuery where it is [publicly available](https://discuss.httparchive.org/t/quickstart-guide-to-exploring-the-http-archive/682).
 
 * **[SpeedCurve](https://speedcurve.com)** — SpeedCurve is a tool for continuously monitoring web performance across different browsers, devices, and regions. It can aggregate any metric including Lighthouse scores across multiple pages and sites, and allows you to set performance budgets with Slack or email alerts. SpeedCurve is a paid product with a free 30-day trial.
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index d4ccf5024..6e5be8c9d 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -82,7 +82,7 @@ declare global {
       /** Whether the page ended up on an HTTPS page after attempting to load the HTTP version. */
       HTTPRedirect: {value: boolean};
       /** Information on size and loading for all the images in the page. */
-      ImageUsage: Artifacts.SingleImageUsage[];
+      ImageElements: Artifacts.ImageElement[];
       /** Information on JS libraries and versions used by the page. */
       JSLibraries: {name: string, version: string, npmPkgName: string}[];
       /** JS coverage information for code used during page load. */
@@ -224,31 +224,33 @@ declare global {
       // TODO(bckenny): real type for parsed manifest.
       export type Manifest = ReturnType<typeof parseManifest>;
 
-      export interface SingleImageUsage {
+      export interface ImageElement {
         src: string;
-        clientWidth: number;
-        clientHeight: number;
+        /** The displayed width of the image, uses img.width when available falling back to clientWidth. See https://codepen.io/patrickhulce/pen/PXvQbM for examples. */
+        displayedWidth: number;
+        /** The displayed height of the image, uses img.height when available falling back to clientHeight. See https://codepen.io/patrickhulce/pen/PXvQbM for examples. */
+        displayedHeight: number;
+        /** The natural width of the underlying image, uses img.naturalWidth. See https://codepen.io/patrickhulce/pen/PXvQbM for examples. */
         naturalWidth: number;
+        /** The natural height of the underlying image, uses img.naturalHeight. See https://codepen.io/patrickhulce/pen/PXvQbM for examples. */
         naturalHeight: number;
-        isCss: boolean;
-        isPicture: boolean;
-        usesObjectFit: boolean;
+        /** The BoundingClientRect of the element. */
         clientRect: {
           top: number;
           bottom: number;
           left: number;
           right: number;
         };
-        networkRecord?: {
-          url: string;
-          resourceSize: number;
-          startTime: number;
-          endTime: number;
-          responseReceivedTime: number;
-          mimeType: string;
-        };
-        width?: number;
-        height?: number;
+        /** Flags whether this element was an image via CSS background-image rather than <img> tag. */
+        isCss: boolean;
+        /** Flags whether this element was contained within a <picture> tag. */
+        isPicture: boolean;
+        /** Flags whether this element was sized using a non-default `object-fit` CSS property. */
+        usesObjectFit: boolean;
+        /** The size of the underlying image file in bytes. 0 if the file could not be identified. */
+        resourceSize: number;
+        /** The MIME type of the underlying image file. */
+        mimeType?: string;
       }
 
       export interface OptimizedImage {


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 884aaa7bd..ff7cc9a7a 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -1057,10 +1057,10 @@ Object {
           "path": "chrome-console-messages",
         },
         Object {
-          "path": "image-usage",
+          "path": "accessibility",
         },
         Object {
-          "path": "accessibility",
+          "path": "image-elements",
         },
         Object {
           "path": "link-elements",
diff --git a/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js b/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
index 8c8e8e9b4..b9f3ccc7e 100644
--- a/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
@@ -12,15 +12,21 @@ const createTestTrace = require('../../create-test-trace.js');
 const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js');
 
 /* eslint-env jest */
-function generateRecord(resourceSizeInKb, startTime = 0, mimeType = 'image/png') {
+function generateRecord({
+  resourceSizeInKb,
+  url = 'https://google.com/logo.png',
+  startTime = 0,
+  mimeType = 'image/png',
+}) {
   return {
+    url,
     mimeType,
     startTime, // DevTools timestamp which is in seconds
     resourceSize: resourceSizeInKb * 1024,
   };
 }
 
-function generateSize(width, height, prefix = 'client') {
+function generateSize(width, height, prefix = 'displayed') {
   const size = {};
   size[`${prefix}Width`] = width;
   size[`${prefix}Height`] = height;
@@ -35,11 +41,11 @@ function generateImage(size, coords, networkRecord, src = 'https://google.com/lo
 
   const clientRect = {
     top: y,
-    bottom: y + size.clientHeight,
+    bottom: y + size.displayedHeight,
     left: x,
-    right: x + size.clientWidth,
+    right: x + size.displayedWidth,
   };
-  const image = {src, networkRecord, clientRect};
+  const image = {src, clientRect, ...networkRecord};
   Object.assign(image, size);
   return image;
 }
@@ -56,7 +62,7 @@ describe('OffscreenImages audit', () => {
     const topLevelTasks = [{ts: 1900, duration: 100}];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
+      ImageElements: [
         generateImage(generateSize(100, 100), [0, 0]),
       ],
       traces: {defaultPass: createTestTrace({topLevelTasks})},
@@ -68,64 +74,73 @@ describe('OffscreenImages audit', () => {
     });
   });
 
-  it('does not find used images', () => {
+  it('does not find used images', async () => {
     const urlB = 'https://google.com/logo2.png';
     const urlC = 'data:image/jpeg;base64,foobar';
+    const recordA = generateRecord({resourceSizeInKb: 100});
+    const recordB = generateRecord({url: urlB, resourceSizeInKb: 100});
+    const recordC = generateRecord({url: urlC, resourceSizeInKb: 3});
     const topLevelTasks = [{ts: 1900, duration: 100}];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
-        generateImage(generateSize(200, 200), [0, 0], generateRecord(100)),
-        generateImage(generateSize(100, 100), [0, 1080], generateRecord(100), urlB),
-        generateImage(generateSize(400, 400), [1720, 1080], generateRecord(3), urlC),
+      ImageElements: [
+        generateImage(generateSize(200, 200), [0, 0], recordA),
+        generateImage(generateSize(100, 100), [0, 1080], recordB, urlB),
+        generateImage(generateSize(400, 400), [1720, 1080], recordC, urlC),
       ],
       traces: {defaultPass: createTestTrace({topLevelTasks})},
       devtoolsLogs: {},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
-      assert.equal(auditResult.items.length, 0);
-    });
+    const auditResult = await UnusedImages.audit_(artifacts, [recordA, recordB, recordC], context);
+    assert.equal(auditResult.items.length, 0);
   });
 
-  it('finds unused images', () => {
+  it('finds unused images', async () => {
     const url = s => `https://google.com/logo${s}.png`;
     const topLevelTasks = [{ts: 1900, duration: 100}];
+    const networkRecords = [
+      generateRecord({url: url(''), resourceSizeInKb: 100}),
+      generateRecord({url: url('B'), resourceSizeInKb: 100}),
+      generateRecord({url: url('C'), resourceSizeInKb: 100}),
+      generateRecord({url: url('D'), resourceSizeInKb: 100}),
+      generateRecord({url: url('E'), resourceSizeInKb: 100}),
+    ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
+      ImageElements: [
         // offscreen to the right
-        generateImage(generateSize(200, 200), [3000, 0], generateRecord(100)),
+        generateImage(generateSize(200, 200), [3000, 0], networkRecords[0]),
         // offscreen to the bottom
-        generateImage(generateSize(100, 100), [0, 2000], generateRecord(100), url('B')),
+        generateImage(generateSize(100, 100), [0, 2000], networkRecords[1], url('B')),
         // offscreen to the top-left
-        generateImage(generateSize(100, 100), [-2000, -1000], generateRecord(100), url('C')),
+        generateImage(generateSize(100, 100), [-2000, -1000], networkRecords[2], url('C')),
         // offscreen to the bottom-right
-        generateImage(generateSize(100, 100), [3000, 2000], generateRecord(100), url('D')),
+        generateImage(generateSize(100, 100), [3000, 2000], networkRecords[3], url('D')),
         // half offscreen to the top, should not warn
-        generateImage(generateSize(1000, 1000), [0, -500], generateRecord(100), url('E')),
+        generateImage(generateSize(1000, 1000), [0, -500], networkRecords[4], url('E')),
       ],
       traces: {defaultPass: createTestTrace({topLevelTasks})},
       devtoolsLogs: {},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
-      assert.equal(auditResult.items.length, 4);
-    });
+    const auditResult = await UnusedImages.audit_(artifacts, networkRecords, context);
+    assert.equal(auditResult.items.length, 4);
   });
 
   it('finds images with 0 area', () => {
     const topLevelTasks = [{ts: 1900, duration: 100}];
+    const networkRecord = generateRecord({resourceSizeInKb: 100});
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
-        generateImage(generateSize(0, 0), [0, 0], generateRecord(100)),
+      ImageElements: [
+        generateImage(generateSize(0, 0), [0, 0], networkRecord),
       ],
       traces: {defaultPass: createTestTrace({topLevelTasks})},
       devtoolsLogs: {},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
+    return UnusedImages.audit_(artifacts, [networkRecord], context).then(auditResult => {
       assert.equal(auditResult.items.length, 1);
       assert.equal(auditResult.items[0].wastedBytes, 100 * 1024);
     });
@@ -134,68 +149,77 @@ describe('OffscreenImages audit', () => {
   it('de-dupes images', () => {
     const urlB = 'https://google.com/logo2.png';
     const topLevelTasks = [{ts: 1900, duration: 100}];
+    const networkRecords = [
+      generateRecord({resourceSizeInKb: 50}),
+      generateRecord({resourceSizeInKb: 50}),
+      generateRecord({url: urlB, resourceSizeInKb: 200}),
+      generateRecord({url: urlB, resourceSizeInKb: 90}),
+    ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
-        generateImage(generateSize(50, 50), [0, 0], generateRecord(50)),
-        generateImage(generateSize(1000, 1000), [1000, 1000], generateRecord(50)),
-        generateImage(generateSize(50, 50), [0, 1500], generateRecord(200), urlB),
-        generateImage(generateSize(400, 400), [0, 1500], generateRecord(90), urlB),
+      ImageElements: [
+        generateImage(generateSize(50, 50), [0, 0], networkRecords[0]),
+        generateImage(generateSize(1000, 1000), [1000, 1000], networkRecords[1]),
+        generateImage(generateSize(50, 50), [0, 1500], networkRecords[2], urlB),
+        generateImage(generateSize(400, 400), [0, 1500], networkRecords[3], urlB),
       ],
       traces: {defaultPass: createTestTrace({topLevelTasks})},
       devtoolsLogs: {},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
+    return UnusedImages.audit_(artifacts, networkRecords, context).then(auditResult => {
       assert.equal(auditResult.items.length, 1);
     });
   });
 
   it('disregards images loaded after TTI', () => {
     const topLevelTasks = [{ts: 1900, duration: 100}];
+    const networkRecord = generateRecord({resourceSizeInKb: 100, startTime: 3});
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
+      ImageElements: [
         // offscreen to the right
-        generateImage(generateSize(200, 200), [3000, 0], generateRecord(100, 3)),
+        generateImage(generateSize(200, 200), [3000, 0], networkRecord),
       ],
       traces: {defaultPass: createTestTrace({topLevelTasks})},
       devtoolsLogs: {},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
+    return UnusedImages.audit_(artifacts, [networkRecord], context).then(auditResult => {
       assert.equal(auditResult.items.length, 0);
     });
   });
 
   it('disregards images loaded after Trace End when interactive throws error', () => {
+    const networkRecord = generateRecord({resourceSizeInKb: 100, startTime: 3});
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
+      ImageElements: [
         // offscreen to the right
-        generateImage(generateSize(200, 200), [3000, 0], generateRecord(100, 3)),
+        generateImage(generateSize(200, 200), [3000, 0], networkRecord),
       ],
       traces: {defaultPass: createTestTrace({traceEnd: 2000})},
       devtoolsLogs: {},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
+    return UnusedImages.audit_(artifacts, [networkRecord], context).then(auditResult => {
       assert.equal(auditResult.items.length, 0);
     });
   });
 
   it('finds images loaded before Trace End when TTI when interactive throws error', () => {
+    const networkRecord = generateRecord({resourceSizeInKb: 100});
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
+      ImageElements: [
         // offscreen to the right
-        generateImage(generateSize(100, 100), [0, 2000], generateRecord(100)),
+        generateImage(generateSize(100, 100), [0, 2000], networkRecord),
       ],
       traces: {defaultPass: createTestTrace({traceEnd: 2000})},
       devtoolsLogs: {},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
+    return UnusedImages.audit_(artifacts, [networkRecord], context).then(auditResult => {
       assert.equal(auditResult.items.length, 1);
     });
   });
@@ -226,7 +250,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
+      ImageElements: [
         generateImage(generateSize(0, 0), [0, 0], recordA, recordA.url),
         generateImage(generateSize(200, 200), [3000, 0], recordB, recordB.url),
       ],
@@ -234,7 +258,7 @@ describe('OffscreenImages audit', () => {
       devtoolsLogs: {defaultPass: devtoolsLog},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
+    return UnusedImages.audit_(artifacts, [recordA, recordB], context).then(auditResult => {
       assert.equal(auditResult.items.length, 1);
       assert.equal(auditResult.items[0].url, recordA.url);
       assert.equal(auditResult.items[0].wastedBytes, wastedSize);
@@ -270,7 +294,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
+      ImageElements: [
         generateImage(generateSize(0, 0), [0, 0], recordA, recordA.url),
         generateImage(generateSize(200, 200), [3000, 0], recordB, recordB.url),
       ],
@@ -278,7 +302,7 @@ describe('OffscreenImages audit', () => {
       devtoolsLogs: {defaultPass: devtoolsLog},
     };
 
-    return UnusedImages.audit_(artifacts, [], context).then(auditResult => {
+    return UnusedImages.audit_(artifacts, [recordA, recordB], context).then(auditResult => {
       assert.equal(auditResult.items.length, 2);
       assert.equal(auditResult.items[0].url, recordA.url);
       assert.equal(auditResult.items[0].wastedBytes, wastedSize);
@@ -289,18 +313,22 @@ describe('OffscreenImages audit', () => {
 
   it('rethrow error when interactive throws error in Lantern', async () => {
     context = {settings: {throttlingMethod: 'simulate'}, computedCache: new Map()};
+    const networkRecords = [
+      generateRecord({url: 'a', resourceSizeInKb: 100, startTime: 3}),
+      generateRecord({url: 'b', resourceSizeInKb: 100, startTime: 4}),
+    ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
-      ImageUsage: [
-        generateImage(generateSize(0, 0), [0, 0], generateRecord(100, 3), 'a'),
-        generateImage(generateSize(200, 200), [3000, 0], generateRecord(100, 4), 'b'),
+      ImageElements: [
+        generateImage(generateSize(0, 0), [0, 0], networkRecords[0], 'a'),
+        generateImage(generateSize(200, 200), [3000, 0], networkRecords[1], 'b'),
       ],
       traces: {defaultPass: createTestTrace({traceEnd: 2000})},
       devtoolsLogs: {},
     };
 
     try {
-      await UnusedImages.audit_(artifacts, [], context);
+      await UnusedImages.audit_(artifacts, networkRecords, context);
     } catch (err) {
       assert.ok(err.message.includes('Did not provide necessary metric computation data'));
       return;
diff --git a/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js b/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js
index 6f4173a8d..68aec09f8 100644
--- a/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/uses-responsive-images-test.js
@@ -19,7 +19,7 @@ function generateRecord(resourceSizeInKb, durationInMs, mimeType = 'image/png')
   };
 }
 
-function generateSize(width, height, prefix = 'client') {
+function generateSize(width, height, prefix = 'displayed') {
   const size = {};
   size[`${prefix}Width`] = width;
   size[`${prefix}Height`] = height;
@@ -28,7 +28,7 @@ function generateSize(width, height, prefix = 'client') {
 
 function generateImage(clientSize, naturalSize, networkRecord, src = 'https://google.com/logo.png') {
   Object.assign(networkRecord || {}, {url: src});
-  const image = {src, networkRecord};
+  const image = {src, ...networkRecord};
   Object.assign(image, clientSize, naturalSize);
   return image;
 }
@@ -39,7 +39,7 @@ describe('Page uses responsive images', () => {
     it(description, () => {
       const result = UsesResponsiveImagesAudit.audit_({
         ViewportDimensions: {devicePixelRatio: data.devicePixelRatio || 1},
-        ImageUsage: [
+        ImageElements: [
           generateImage(
             generateSize(...data.clientSize),
             generateSize(...data.naturalSize, 'natural'),
@@ -92,7 +92,7 @@ describe('Page uses responsive images', () => {
   it('handles images without network record', () => {
     const auditResult = UsesResponsiveImagesAudit.audit_({
       ViewportDimensions: {devicePixelRatio: 2},
-      ImageUsage: [
+      ImageElements: [
         generateImage(
           generateSize(100, 100),
           generateSize(300, 300, 'natural'),
@@ -107,7 +107,7 @@ describe('Page uses responsive images', () => {
   it('identifies when images are not wasteful', () => {
     const auditResult = UsesResponsiveImagesAudit.audit_({
       ViewportDimensions: {devicePixelRatio: 2},
-      ImageUsage: [
+      ImageElements: [
         generateImage(
           generateSize(200, 200),
           generateSize(450, 450, 'natural'),
@@ -139,7 +139,7 @@ describe('Page uses responsive images', () => {
 
     const auditResult = UsesResponsiveImagesAudit.audit_({
       ViewportDimensions: {devicePixelRatio: 1},
-      ImageUsage: [
+      ImageElements: [
         generateImage(generateSize(10, 10), naturalSizeA, recordA, urlA),
       ],
     });
@@ -157,7 +157,7 @@ describe('Page uses responsive images', () => {
 
     const auditResult = UsesResponsiveImagesAudit.audit_({
       ViewportDimensions: {devicePixelRatio: 1},
-      ImageUsage: [
+      ImageElements: [
         generateImage(generateSize(10, 10), naturalSizeA, recordA, urlA),
         generateImage(generateSize(450, 450), naturalSizeA, recordA, urlA),
         generateImage(generateSize(30, 30), naturalSizeA, recordA, urlA),
diff --git a/lighthouse-core/test/audits/image-aspect-ratio-test.js b/lighthouse-core/test/audits/image-aspect-ratio-test.js
index ac438abe9..6b3902db4 100644
--- a/lighthouse-core/test/audits/image-aspect-ratio-test.js
+++ b/lighthouse-core/test/audits/image-aspect-ratio-test.js
@@ -9,16 +9,9 @@ const ImageAspectRatioAudit = require('../../audits/image-aspect-ratio.js');
 const assert = require('assert');
 
 /* eslint-env jest */
-function generateRecord(url = 'https://google.com/logo.png', mimeType = 'image/png') {
-  return {
-    url,
-    mimeType,
-  };
-}
 
-function generateImage(clientSize, naturalSize, networkRecord, props, src = 'https://google.com/logo.png') {
-  Object.assign(networkRecord || {}, {url: src});
-  const image = {src, networkRecord};
+function generateImage(clientSize, naturalSize, props, src = 'https://google.com/logo.png') {
+  const image = {src, mimeType: 'image/png'};
   Object.assign(image, clientSize, naturalSize, props);
   return image;
 }
@@ -28,11 +21,10 @@ describe('Images: aspect-ratio audit', () => {
     const description = `identifies when an image ${condition}`;
     it(description, () => {
       const result = ImageAspectRatioAudit.audit({
-        ImageUsage: [
+        ImageElements: [
           generateImage(
-            {width: data.clientSize[0], height: data.clientSize[1]},
+            {displayedWidth: data.clientSize[0], displayedHeight: data.clientSize[1]},
             {naturalWidth: data.naturalSize[0], naturalHeight: data.naturalSize[1]},
-            generateRecord(),
             data.props
           ),
         ],
@@ -149,15 +141,12 @@ describe('Images: aspect-ratio audit', () => {
 
   it('skips svg images', () => {
     const result = ImageAspectRatioAudit.audit({
-      ImageUsage: [
+      ImageElements: [
         generateImage(
           {width: 150, height: 150},
           {},
           {
-            url: 'https://google.com/logo.png',
             mimeType: 'image/svg+xml',
-          },
-          {
             isCss: false,
             usesObjectFit: false,
           }
diff --git a/lighthouse-core/test/gather/gatherers/seo/font-size-test.js b/lighthouse-core/test/gather/gatherers/seo/font-size-test.js
index 4f0b74b2d..58b09939f 100644
--- a/lighthouse-core/test/gather/gatherers/seo/font-size-test.js
+++ b/lighthouse-core/test/gather/gatherers/seo/font-size-test.js
@@ -10,9 +10,9 @@
 const FontSizeGather = require('../../../../gather/gatherers/seo/font-size');
 let fontSizeGather;
 
-const smallText = ' body small text ';
-const bigText = 'body big text';
-const failingText = 'failing text';
+const smallText = ' body sm𝐀ll text ';
+const bigText = 'body 𝐁ig text';
+const failingText = 'failing text 💩';
 const bodyNode = {nodeId: 3, nodeName: 'BODY', parentId: 1};
 const failingNode = {nodeId: 10, nodeName: 'P', parentId: 3};
 const nodes = [
@@ -93,9 +93,10 @@ describe('Font size gatherer', () => {
     };
 
     const artifact = await fontSizeGather.afterPass({driver});
-    const expectedFailingTextLength = smallText.trim().length;
-    const expectedVisitedTextLength = bigText.trim().length + expectedFailingTextLength;
-    const expectedTotalTextLength = failingText.trim().length + expectedVisitedTextLength;
+    const expectedFailingTextLength = Array.from(smallText.trim()).length;
+    const expectedVisitedTextLength = Array.from(bigText.trim()).length + expectedFailingTextLength;
+    const expectedTotalTextLength = Array.from(failingText.trim()).length +
+      expectedVisitedTextLength;
     const expectedAnalyzedFailingTextLength = expectedFailingTextLength;
 
     expect(artifact).toEqual({
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index 52e44891f..1cca9a73c 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -777,13 +777,11 @@
       }
     }
   ],
-  "ImageUsage": [
+  "ImageElements": [
     {
       "src": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg",
-      "width": 480,
-      "height": 57,
-      "clientWidth": 480,
-      "clientHeight": 57,
+      "displayedWidth": 480,
+      "displayedHeight": 57,
       "clientRect": {
         "top": 103.8125,
         "bottom": 160.8125,
@@ -795,21 +793,13 @@
       "isCss": false,
       "isPicture": false,
       "usesObjectFit": false,
-      "networkRecord": {
-        "url": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg",
-        "resourceSize": 24620,
-        "startTime": 185607.272846,
-        "endTime": 185608.100862,
-        "responseReceivedTime": 185607.847168,
-        "mimeType": "image/jpeg"
-      }
+      "resourceSize": 24620,
+      "mimeType": "image/jpeg"
     },
     {
       "src": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg",
-      "width": 480,
-      "height": 318,
-      "clientWidth": 480,
-      "clientHeight": 318,
+      "displayedWidth": 480,
+      "displayedHeight": 318,
       "clientRect": {
         "top": 164.8125,
         "bottom": 482.8125,
@@ -821,21 +811,13 @@
       "isCss": false,
       "isPicture": false,
       "usesObjectFit": false,
-      "networkRecord": {
-        "url": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg",
-        "resourceSize": 24620,
-        "startTime": 185607.272846,
-        "endTime": 185608.100862,
-        "responseReceivedTime": 185607.847168,
-        "mimeType": "image/jpeg"
-      }
+      "resourceSize": 24620,
+      "mimeType": "image/jpeg"
     },
     {
       "src": "blob:http://localhost:10200/ae0eac03-ab9b-4a6a-b299-f5212153e277",
-      "width": 16,
-      "height": 16,
-      "clientWidth": 16,
-      "clientHeight": 16,
+      "displayedWidth": 16,
+      "displayedHeight": 16,
       "clientRect": {
         "top": 559.8125,
         "bottom": 575.8125,