Repository: GoogleChrome/lighthouse
Pull request #12570 (2021-05-26 23:54:50)

clients(psi): render treemap button
ref #11254


Patch:
diff --git a/lighthouse-core/audits/byte-efficiency/modern-image-formats.js b/lighthouse-core/audits/byte-efficiency/modern-image-formats.js
index 316920635..74b1c5178 100644
--- a/lighthouse-core/audits/byte-efficiency/modern-image-formats.js
+++ b/lighthouse-core/audits/byte-efficiency/modern-image-formats.js
@@ -95,8 +95,10 @@ class ModernImageFormats extends ByteEfficiencyAudit {
           continue;
         }
 
-        const naturalHeight = imageElement.naturalHeight;
-        const naturalWidth = imageElement.naturalWidth;
+        // Skip if we couldn't collect natural image size information.
+        if (!imageElement.naturalDimensions) continue;
+        const naturalHeight = imageElement.naturalDimensions.height;
+        const naturalWidth = imageElement.naturalDimensions.width;
         // If naturalHeight or naturalWidth are falsy, information is not valid, skip.
         if (!naturalWidth || !naturalHeight) continue;
 
diff --git a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
index 8db7333a6..722f00ab6 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-optimized-images.js
@@ -96,9 +96,11 @@ class UsesOptimizedImages extends ByteEfficiencyAudit {
           continue;
         }
 
+        // Skip if we couldn't collect natural image size information.
+        if (!imageElement.naturalDimensions) continue;
+        const naturalHeight = imageElement.naturalDimensions.height;
+        const naturalWidth = imageElement.naturalDimensions.width;
         // If naturalHeight or naturalWidth are falsy, information is not valid, skip.
-        const naturalHeight = imageElement.naturalHeight;
-        const naturalWidth = imageElement.naturalWidth;
         if (!naturalHeight || !naturalWidth) continue;
         jpegSize =
           UsesOptimizedImages.estimateJPEGSizeFromDimensions({naturalHeight, naturalWidth});
diff --git a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
index 1d74a44c0..64c67e43c 100644
--- a/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
+++ b/lighthouse-core/audits/byte-efficiency/uses-responsive-images.js
@@ -117,8 +117,10 @@ class UsesResponsiveImages extends ByteEfficiencyAudit {
         continue;
       }
 
-      const naturalHeight = image.naturalHeight;
-      const naturalWidth = image.naturalWidth;
+      // Skip if we couldn't collect natural image size information.
+      if (!image.naturalDimensions) continue;
+      const naturalHeight = image.naturalDimensions.height;
+      const naturalWidth = image.naturalDimensions.width;
       // If naturalHeight or naturalWidth are falsy, information is not valid, skip.
       if (!naturalWidth || !naturalHeight) continue;
       const processed =
diff --git a/lighthouse-core/audits/image-aspect-ratio.js b/lighthouse-core/audits/image-aspect-ratio.js
index e510ce1eb..cffb36a23 100644
--- a/lighthouse-core/audits/image-aspect-ratio.js
+++ b/lighthouse-core/audits/image-aspect-ratio.js
@@ -55,7 +55,7 @@ class ImageAspectRatio extends Audit {
    */
   static computeAspectRatios(image) {
     const url = URL.elideDataURI(image.src);
-    const actualAspectRatio = image.naturalWidth / image.naturalHeight;
+    const actualAspectRatio = image.naturalDimensions.width / image.naturalDimensions.height;
     const displayedAspectRatio = image.displayedWidth / image.displayedHeight;
 
     const targetDisplayHeight = image.displayedWidth / actualAspectRatio;
@@ -65,7 +65,7 @@ class ImageAspectRatio extends Audit {
       url,
       displayedAspectRatio: `${image.displayedWidth} x ${image.displayedHeight}
         (${displayedAspectRatio.toFixed(2)})`,
-      actualAspectRatio: `${image.naturalWidth} x ${image.naturalHeight}
+      actualAspectRatio: `${image.naturalDimensions.width} x ${image.naturalDimensions.height}
         (${actualAspectRatio.toFixed(2)})`,
       doRatiosMatch,
     };
@@ -90,11 +90,12 @@ class ImageAspectRatio extends Audit {
       return !image.isCss &&
         image.mimeType &&
         image.mimeType !== 'image/svg+xml' &&
-        image.naturalHeight && image.naturalHeight > 5 &&
-        image.naturalWidth && image.naturalWidth > 5 &&
+        image.naturalDimensions &&
+        image.naturalDimensions.height > 5 &&
+        image.naturalDimensions.width > 5 &&
         image.displayedWidth &&
         image.displayedHeight &&
-        image.cssComputedObjectFit === 'fill';
+        image.computedStyles.objectFit === 'fill';
     }).forEach(image => {
       const wellDefinedImage = /** @type {WellDefinedImage} */ (image);
       const processed = ImageAspectRatio.computeAspectRatios(wellDefinedImage);
diff --git a/lighthouse-core/audits/image-size-responsive.js b/lighthouse-core/audits/image-size-responsive.js
index 98559e2aa..9a85d9052 100644
--- a/lighthouse-core/audits/image-size-responsive.js
+++ b/lighthouse-core/audits/image-size-responsive.js
@@ -14,6 +14,8 @@ const Audit = require('./audit.js');
 const URL = require('../lib/url-shim.js');
 const i18n = require('../lib/i18n/i18n.js');
 
+/** @typedef {LH.Artifacts.ImageElement & Required<Pick<LH.Artifacts.ImageElement, 'naturalDimensions'>>} ImageWithNaturalDimensions */
+
 const UIStrings = {
   /** Title of a Lighthouse audit that provides detail on the size of visible images on the page. This descriptive title is shown to users when all images have correct sizes. */
   title: 'Serves images with appropriate resolution',
@@ -86,7 +88,11 @@ function isCandidate(image) {
   if (image.displayedWidth <= 1 || image.displayedHeight <= 1) {
     return false;
   }
-  if (!image.naturalWidth || !image.naturalHeight) {
+  if (
+    !image.naturalDimensions ||
+    !image.naturalDimensions.width ||
+    !image.naturalDimensions.height
+  ) {
     return false;
   }
   if (image.mimeType === 'image/svg+xml') {
@@ -95,11 +101,11 @@ function isCandidate(image) {
   if (image.isCss) {
     return false;
   }
-  if (image.cssComputedObjectFit !== 'fill') {
+  if (image.computedStyles.objectFit !== 'fill') {
     return false;
   }
   // Check if pixel art scaling is used.
-  if (artisticImageRenderingValues.includes(image.cssComputedImageRendering)) {
+  if (artisticImageRenderingValues.includes(image.computedStyles.imageRendering)) {
     return false;
   }
   // Check if density descriptor is used.
@@ -113,25 +119,26 @@ function isCandidate(image) {
  * Type check to ensure that the ImageElement has natural dimensions.
  *
  * @param {LH.Artifacts.ImageElement} image
- * @return {image is LH.Artifacts.ImageElement & {naturalWidth: number, naturalHeight: number}}
+ * @return {image is ImageWithNaturalDimensions}
  */
 function imageHasNaturalDimensions(image) {
-  return image.naturalHeight !== undefined && image.naturalWidth !== undefined;
+  return !!image.naturalDimensions;
 }
 
 /**
- * @param {LH.Artifacts.ImageElement & {naturalHeight: number, naturalWidth: number}} image
+ * @param {ImageWithNaturalDimensions} image
  * @param {number} DPR
  * @return {boolean}
  */
 function imageHasRightSize(image, DPR) {
   const [expectedWidth, expectedHeight] =
       allowedImageSize(image.displayedWidth, image.displayedHeight, DPR);
-  return image.naturalWidth >= expectedWidth && image.naturalHeight >= expectedHeight;
+  return image.naturalDimensions.width >= expectedWidth &&
+    image.naturalDimensions.height >= expectedHeight;
 }
 
 /**
- * @param {LH.Artifacts.ImageElement & {naturalWidth: number, naturalHeight: number}} image
+ * @param {ImageWithNaturalDimensions} image
  * @param {number} DPR
  * @return {Result}
  */
@@ -142,8 +149,8 @@ function getResult(image, DPR) {
     url: image.src,
     elidedUrl: URL.elideDataURI(image.src),
     displayedSize: `${image.displayedWidth} x ${image.displayedHeight}`,
-    actualSize: `${image.naturalWidth} x ${image.naturalHeight}`,
-    actualPixels: image.naturalWidth * image.naturalHeight,
+    actualSize: `${image.naturalDimensions.width} x ${image.naturalDimensions.height}`,
+    actualPixels: image.naturalDimensions.width * image.naturalDimensions.height,
     expectedSize: `${expectedWidth} x ${expectedHeight}`,
     expectedPixels: expectedWidth * expectedHeight,
   };
diff --git a/lighthouse-core/audits/unsized-images.js b/lighthouse-core/audits/unsized-images.js
index 8613ecc5d..d6601e447 100644
--- a/lighthouse-core/audits/unsized-images.js
+++ b/lighthouse-core/audits/unsized-images.js
@@ -42,10 +42,12 @@ class UnsizedImages extends Audit {
   /**
    * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
    * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
-   * @param {string} attrValue
+   * @param {string | null} attrValue
    * @return {boolean}
    */
   static doesHtmlAttrProvideExplicitSize(attrValue) {
+    if (!attrValue) return false;
+
     // First, superweird edge case of using the positive-sign. The spec _sorta_ says it's valid...
     // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers
     //   > Otherwise, if the character is … (+): Advance position to the next character.
@@ -80,13 +82,13 @@ class UnsizedImages extends Audit {
     // We don't want to show the user a false positive, so we'll call it sized to give it as pass.
     // While this situation should only befall small-impact images, it means our analysis is incomplete. :(
     // Handwavey TODO: explore ways to avoid this.
-    if (image._privateCssSizing === undefined) return true;
+    if (image.cssEffectiveRules === undefined) return true;
 
     const attrWidth = image.attributeWidth;
     const attrHeight = image.attributeHeight;
-    const cssWidth = image._privateCssSizing.width;
-    const cssHeight = image._privateCssSizing.height;
-    const cssAspectRatio = image._privateCssSizing.aspectRatio;
+    const cssWidth = image.cssEffectiveRules.width;
+    const cssHeight = image.cssEffectiveRules.height;
+    const cssAspectRatio = image.cssEffectiveRules.aspectRatio;
     const htmlWidthIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrWidth);
     const cssWidthIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssWidth);
     const htmlHeightIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrHeight);
@@ -122,7 +124,7 @@ class UnsizedImages extends Audit {
     for (const image of images) {
       // Fixed images are out of document flow and won't cause layout shifts
       const isFixedImage =
-        image.cssComputedPosition === 'fixed' || image.cssComputedPosition === 'absolute';
+        image.computedStyles.position === 'fixed' || image.computedStyles.position === 'absolute';
       if (isFixedImage) continue;
 
       // Non-network SVGs with dimensions don't cause layout shifts in practice, skip them.
diff --git a/lighthouse-core/gather/gatherers/image-elements.js b/lighthouse-core/gather/gatherers/image-elements.js
index 50cb0f094..2a47ebec0 100644
--- a/lighthouse-core/gather/gatherers/image-elements.js
+++ b/lighthouse-core/gather/gatherers/image-elements.js
@@ -70,19 +70,20 @@ function getHTMLImages(allElements) {
       displayedWidth: element.width,
       displayedHeight: element.height,
       clientRect: getClientRect(element),
-      naturalWidth: canTrustNaturalDimensions ? element.naturalWidth : undefined,
-      naturalHeight: canTrustNaturalDimensions ? element.naturalHeight : undefined,
-      attributeWidth: element.getAttribute('width') || '',
-      attributeHeight: element.getAttribute('height') || '',
-      cssWidth: undefined, // this will get overwritten below
-      cssHeight: undefined, // this will get overwritten below
-      _privateCssSizing: undefined, // this will get overwritten below
-      cssComputedPosition: getPosition(element, computedStyle),
+      attributeWidth: element.getAttribute('width'),
+      attributeHeight: element.getAttribute('height'),
+      naturalDimensions: canTrustNaturalDimensions ?
+        {width: element.naturalWidth, height: element.naturalHeight} :
+        undefined,
+      cssRules: undefined, // this will get overwritten below
+      computedStyles: {
+        position: getPosition(element, computedStyle),
+        objectFit: computedStyle.getPropertyValue('object-fit'),
+        imageRendering: computedStyle.getPropertyValue('image-rendering'),
+      },
       isCss: false,
       isPicture,
       loading: element.loading,
-      cssComputedObjectFit: computedStyle.getPropertyValue('object-fit'),
-      cssComputedImageRendering: computedStyle.getPropertyValue('image-rendering'),
       isInShadowDOM: element.getRootNode() instanceof ShadowRoot,
       // @ts-expect-error - getNodeDetails put into scope via stringification
       node: getNodeDetails(element),
@@ -119,17 +120,18 @@ function getCSSImages(allElements) {
       displayedWidth: element.clientWidth,
       displayedHeight: element.clientHeight,
       clientRect: getClientRect(element),
-      attributeWidth: '',
-      attributeHeight: '',
-      cssWidth: undefined,
-      cssHeight: undefined,
-      _privateCssSizing: undefined,
-      cssComputedPosition: getPosition(element, style),
+      attributeWidth: null,
+      attributeHeight: null,
+      naturalDimensions: undefined,
+      cssEffectiveRules: undefined,
+      computedStyles: {
+        position: getPosition(element, style),
+        objectFit: '',
+        imageRendering: style.getPropertyValue('image-rendering'),
+      },
       isCss: true,
       isPicture: false,
       isInShadowDOM: element.getRootNode() instanceof ShadowRoot,
-      cssComputedObjectFit: '',
-      cssComputedImageRendering: style.getPropertyValue('image-rendering'),
       // @ts-expect-error - getNodeDetails put into scope via stringification
       node: getNodeDetails(element),
     });
@@ -241,23 +243,22 @@ class ImageElements extends FRGatherer {
    */
   async fetchElementWithSizeInformation(driver, element) {
     const url = element.src;
-    if (this._naturalSizeCache.has(url)) {
-      Object.assign(element, this._naturalSizeCache.get(url));
-      return;
+    let size = this._naturalSizeCache.get(url);
+    if (!size) {
+      try {
+        // We don't want this to take forever, 250ms should be enough for images that are cached
+        driver.defaultSession.setNextProtocolTimeout(250);
+        size = await driver.executionContext.evaluate(determineNaturalSize, {
+          args: [url],
+        });
+        this._naturalSizeCache.set(url, size);
+      } catch (_) {
+        // determineNaturalSize fails on invalid images, which we treat as non-visible
+      }
     }
 
-    try {
-      // We don't want this to take forever, 250ms should be enough for images that are cached
-      driver.defaultSession.setNextProtocolTimeout(250);
-      const size = await driver.executionContext.evaluate(determineNaturalSize, {
-        args: [url],
-      });
-      this._naturalSizeCache.set(url, size);
-      Object.assign(element, size);
-    } catch (_) {
-      // determineNaturalSize fails on invalid images, which we treat as non-visible
-      return;
-    }
+    if (!size) return;
+    element.naturalDimensions = {width: size.naturalWidth, height: size.naturalHeight};
   }
 
   /**
@@ -281,10 +282,7 @@ class ImageElements extends FRGatherer {
       const width = getEffectiveSizingRule(matchedRules, 'width');
       const height = getEffectiveSizingRule(matchedRules, 'height');
       const aspectRatio = getEffectiveSizingRule(matchedRules, 'aspect-ratio');
-      // COMPAT: Maintain backcompat for <= 7.0.1
-      element.cssWidth = width === null ? undefined : width;
-      element.cssHeight = height === null ? undefined : height;
-      element._privateCssSizing = {width, height, aspectRatio};
+      element.cssEffectiveRules = {width, height, aspectRatio};
     } catch (err) {
       if (/No node.*found/.test(err.message)) return;
       throw err;
diff --git a/lighthouse-core/report/html/renderer/psi.js b/lighthouse-core/report/html/renderer/psi.js
index fc719caa3..f0b3561e5 100644
--- a/lighthouse-core/report/html/renderer/psi.js
+++ b/lighthouse-core/report/html/renderer/psi.js
@@ -16,8 +16,7 @@
  */
 'use strict';
 
-/* globals self DOM PerformanceCategoryRenderer Util I18n DetailsRenderer ElementScreenshotRenderer */
-
+/* globals self DOM PerformanceCategoryRenderer Util I18n DetailsRenderer ElementScreenshotRenderer ReportUIFeatures */
 
 /**
  * Returns all the elements that PSI needs to render the report
@@ -81,6 +80,9 @@ function prepareLabData(LHResult, document) {
   const clonedScoreTemplate = dom.cloneTemplate('#tmpl-lh-scorescale', dom.document());
   const scoreScaleEl = dom.find('.lh-scorescale', clonedScoreTemplate);
 
+  const reportUIFeatures = new ReportUIFeatures(dom);
+  reportUIFeatures.json = lhResult;
+
   /** @param {HTMLElement} reportEl */
   const installFeatures = (reportEl) => {
     if (fullPageScreenshot) {
@@ -109,6 +111,17 @@ function prepareLabData(LHResult, document) {
       ElementScreenshotRenderer.installFullPageScreenshot(
         screenshotEl, fullPageScreenshot.screenshot);
     }
+
+    const showTreemapApp =
+      lhResult.audits['script-treemap-data'] && lhResult.audits['script-treemap-data'].details;
+    if (showTreemapApp) {
+      reportUIFeatures.addButton({
+        container: reportEl.querySelector('.lh-audit-group--metrics'),
+        text: Util.i18n.strings.viewTreemapLabel,
+        icon: 'treemap',
+        onClick: () => ReportUIFeatures.openTreemap(lhResult, 'url'),
+      });
+    }
   };
 
   return {scoreGaugeEl, perfCategoryEl, finalScreenshotDataUri, scoreScaleEl, installFeatures};
@@ -126,6 +139,13 @@ function _getFinalScreenshot(perfCategory) {
   return details.data;
 }
 
+// Defined by lib/file-namer.js, but that file does not exist in PSI. PSI doesn't use it, but
+// needs some basic definition so closure compiler accepts report-ui-features.js
+// @ts-expect-error - unused by typescript, used by closure compiler
+// eslint-disable-next-line no-unused-vars
+function getFilenamePrefix(lhr) {
+}
+
 if (typeof module !== 'undefined' && module.exports) {
   module.exports = prepareLabData;
 } else {
diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js
index 4559298db..04c9f3d7e 100644
--- a/lighthouse-core/report/html/renderer/report-ui-features.js
+++ b/lighthouse-core/report/html/renderer/report-ui-features.js
@@ -181,14 +181,17 @@ class ReportUIFeatures {
   }
 
   /**
-   * @param {{text: string, icon?: string, onClick: () => void}} opts
+   * @param {{container?: Element, text: string, icon?: string, onClick: () => void}} opts
    */
   addButton(opts) {
+    // report-ui-features doesn't have a reference to the root report el, and PSI has
+    // 2 reports on the page (and not even attached to DOM when installFeatures is called..)
+    // so we need a container option to specify where the element should go.
     const metricsEl = this._document.querySelector('.lh-audit-group--metrics');
-    // Not supported without metrics group.
+    const containerEl = opts.container || metricsEl;
     if (!metricsEl) return;
 
-    let buttonsEl = metricsEl.querySelector('.lh-buttons');
+    let buttonsEl = containerEl.querySelector('.lh-buttons');
     if (!buttonsEl) buttonsEl = this._dom.createChildOf(metricsEl, 'div', 'lh-buttons');
 
     const classes = [
@@ -551,7 +554,6 @@ class ReportUIFeatures {
    * Opens a new tab to the treemap app and sends the JSON results using postMessage.
    * @param {LH.Result} json
    * @param {'postMessage'|'url'} method
-   * @protected
    */
   static openTreemap(json, method = 'postMessage') {
     const treemapData = json.audits['script-treemap-data'].details;
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index a21cc5b42..24e7dfc1c 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -434,6 +434,7 @@ declare global {
       }
 
       export interface ImageElement {
+        /** The resolved source URL of the image. Similar to `currentSrc`, but resolved for CSS images as well. */
         src: string;
         /** The srcset attribute value. @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset */
         srcset: string;
@@ -441,39 +442,41 @@ declare global {
         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 raw width attribute of the image element. CSS images will be set to null. */
+        attributeWidth: string | null;
+        /** The raw height attribute of the image element. CSS images will be set to null. */
+        attributeHeight: string | null;
         /**
-         * The natural height of the underlying image, uses img.naturalHeight. See https://codepen.io/patrickhulce/pen/PXvQbM for examples.
-         * TODO: explore revising the shape of this data. https://github.com/GoogleChrome/lighthouse/issues/12077
+         * The natural width and height of the underlying image resource, uses img.naturalHeight/img.naturalWidth. See https://codepen.io/patrickhulce/pen/PXvQbM for examples.
+         * Set to `undefined` if the data could not be collected.
          */
-        naturalHeight?: number;
-        /** The raw width attribute of the image element. CSS images will be set to the empty string. */
-        attributeWidth: string;
-        /** The raw height attribute of the image element. CSS images will be set to the empty string. */
-        attributeHeight: string;
-        /**
-         * The CSS width property of the image element.
-         * TODO: explore deprecating this in favor of _privateCssSizing. https://github.com/GoogleChrome/lighthouse/issues/12077
-         */
-        cssWidth?: string;
-        /**
-         * The CSS height property of the image element.
-         * TODO: explore deprecating this in favor of _privateCssSizing
-         */
-        cssHeight?: string;
+        naturalDimensions?: {
+          width: number;
+          height: number;
+        };
         /**
-         * The width/height of the element as defined by matching CSS rules. Set to `undefined` if the data was not collected.
-         * TODO: Finalize naming/shape of this data prior to Lighthouse 8. https://github.com/GoogleChrome/lighthouse/issues/12077
+         * The width/height of the element as defined by matching CSS rules.
+         * These are distinct from the `computedStyles` properties in that they are the raw property value.
+         * e.g. `width` would be `"100vw"`, not the computed width in pixels.
+         * Set to `undefined` if the data was not collected.
          */
-        _privateCssSizing?: {
+        cssEffectiveRules?: {
           /** The width of the image as expressed by CSS rules. Set to `null` if there was no width set in CSS. */
           width: string | null;
           /** The height of the image as expressed by CSS rules. Set to `null` if there was no height set in CSS. */
           height: string | null;
           /** The aspect ratio of the image as expressed by CSS rules. Set to `null` if there was no aspect ratio set in CSS. */
           aspectRatio: string | null;
-        }
+        };
+        /** The computed styles as determined by `getComputedStyle`. */
+        computedStyles: {
+          /** CSS `position` property. */
+          position: string;
+          /** CSS `object-fit` property. */
+          objectFit: string;
+          /** CSS `image-rendering` propertry. */
+          imageRendering: string;
+        };
         /** The BoundingClientRect of the element. */
         clientRect: {
           top: number;
@@ -481,18 +484,12 @@ declare global {
           left: number;
           right: number;
         };
-        /** The CSS position attribute of the element */
-        cssComputedPosition: string;
         /** 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 contained within a ShadowRoot */
         isInShadowDOM: boolean;
-        /** `object-fit` CSS property. */
-        cssComputedObjectFit: string;
-        /** `image-rendering` propertry. */
-        cssComputedImageRendering: string;
         /** The MIME type of the underlying image file. */
         mimeType?: string;
         /** Details for node in DOM for the image element */


Test patch:
diff --git a/lighthouse-core/test/audits/byte-efficiency/modern-image-formats-test.js b/lighthouse-core/test/audits/byte-efficiency/modern-image-formats-test.js
index 69eb2f77f..ab6aa6b57 100644
--- a/lighthouse-core/test/audits/byte-efficiency/modern-image-formats-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/modern-image-formats-test.js
@@ -32,8 +32,7 @@ function generateArtifacts(images) {
 
     imageElements.push({
       src: url,
-      naturalWidth: image.width,
-      naturalHeight: image.height,
+      naturalDimensions: image,
     });
   }
 
diff --git a/lighthouse-core/test/audits/byte-efficiency/uses-optimized-images-test.js b/lighthouse-core/test/audits/byte-efficiency/uses-optimized-images-test.js
index 4b9d1045e..a7a3be3a4 100644
--- a/lighthouse-core/test/audits/byte-efficiency/uses-optimized-images-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/uses-optimized-images-test.js
@@ -32,8 +32,7 @@ function generateArtifacts(images) {
 
     imageElements.push({
       src: url,
-      naturalWidth: image.width,
-      naturalHeight: image.height,
+      naturalDimensions: image,
     });
   }
 
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 ff8e59583..b24dc4aa7 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
@@ -28,8 +28,8 @@ function generateSize(width, height, prefix = 'displayed') {
   return size;
 }
 
-function generateImage(clientSize, naturalSize, src = 'https://google.com/logo.png') {
-  return {src, ...clientSize, ...naturalSize};
+function generateImage(clientSize, naturalDimensions, src = 'https://google.com/logo.png') {
+  return {src, ...clientSize, naturalDimensions};
 }
 
 describe('Page uses responsive images', () => {
@@ -44,14 +44,14 @@ describe('Page uses responsive images', () => {
       ImageElements: [
         generateImage(
           generateSize(...data.clientSize),
-          generateSize(...data.naturalSize, 'natural')
+          {width: data.naturalSize[0], height: data.naturalSize[1]}
         ),
       ],
     };
     it(description, () => {
       // eslint-disable-next-line max-len
       const result = UsesResponsiveImagesAudit.audit_(artifacts, [generateRecord(data.sizeInKb, data.durationInMs || 200)]);
-      assert.equal(result.items.length, data.listed ? 1 : 0);
+      expect(result.items).toHaveLength(data.listed ? 1 : 0);
       if (data.listed) {
         assert.equal(Math.round(result.items[0].wastedBytes / 1024), data.expectedWaste);
       }
@@ -115,7 +115,7 @@ describe('Page uses responsive images', () => {
       ImageElements: [
         generateImage(
           generateSize(100, 100),
-          generateSize(300, 300, 'natural'),
+          {width: 300, height: 300},
           null
         ),
       ],
@@ -133,17 +133,17 @@ describe('Page uses responsive images', () => {
       ImageElements: [
         generateImage(
           generateSize(200, 200),
-          generateSize(450, 450, 'natural'),
+          {width: 450, height: 450},
           'https://google.com/logo.png'
         ),
         generateImage(
           generateSize(100, 100),
-          generateSize(210, 210, 'natural'),
+          {width: 210, height: 210},
           'https://google.com/logo2.png'
         ),
         generateImage(
           generateSize(100, 100),
-          generateSize(80, 80, 'natural'),
+          {width: 80, height: 80},
           'data:image/jpeg;base64,foobar'
         ),
       ],
@@ -156,7 +156,7 @@ describe('Page uses responsive images', () => {
 
   it('ignores vectors', () => {
     const urlA = 'https://google.com/logo.svg';
-    const naturalSizeA = generateSize(450, 450, 'natural');
+    const naturalSizeA = {width: 450, height: 450};
     const image =
       {...generateImage(generateSize(10, 10), naturalSizeA, urlA), mimeType: 'image/svg+xml'};
     const auditResult = UsesResponsiveImagesAudit.audit_({
@@ -172,7 +172,7 @@ describe('Page uses responsive images', () => {
 
   it('ignores CSS', () => {
     const urlA = 'https://google.com/logo.png';
-    const naturalSizeA = generateSize(450, 450, 'natural');
+    const naturalSizeA = {width: 450, height: 450};
 
     const auditResult = UsesResponsiveImagesAudit.audit_({
       ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 1},
@@ -188,7 +188,7 @@ describe('Page uses responsive images', () => {
 
   it('handles failure', () => {
     const urlA = 'https://google.com/logo.png';
-    const naturalSizeA = generateSize(NaN, 450, 'natural');
+    const naturalSizeA = {width: NaN, height: 450};
     const auditResult = UsesResponsiveImagesAudit.audit_({
       ViewportDimensions: {innerWidth: 1000, innerHeight: 1000, devicePixelRatio: 1},
       ImageElements: [
@@ -203,10 +203,10 @@ describe('Page uses responsive images', () => {
 
   it('de-dupes images', () => {
     const urlA = 'https://google.com/logo.png';
-    const naturalSizeA = generateSize(450, 450, 'natural');
+    const naturalSizeA = {width: 450, height: 450};
     const recordA = generateRecord(100, 300, urlA);
     const urlB = 'https://google.com/logoB.png';
-    const naturalSizeB = generateSize(1000, 1000, 'natural');
+    const naturalSizeB = {width: 1000, height: 1000};
     const recordB = generateRecord(10, 20, urlB); // make it small to keep test passing
     const networkRecords = [recordA, recordB];
 
diff --git a/lighthouse-core/test/audits/image-aspect-ratio-test.js b/lighthouse-core/test/audits/image-aspect-ratio-test.js
index ce2941bde..b35ed93cd 100644
--- a/lighthouse-core/test/audits/image-aspect-ratio-test.js
+++ b/lighthouse-core/test/audits/image-aspect-ratio-test.js
@@ -10,13 +10,13 @@ const assert = require('assert').strict;
 
 /* eslint-env jest */
 
-function generateImage(clientSize, naturalSize, props, src = 'https://google.com/logo.png') {
+function generateImage(clientSize, naturalDimensions, props, src = 'https://google.com/logo.png') {
   return {
     src,
     mimeType: 'image/png',
-    cssComputedObjectFit: 'fill',
+    computedStyles: {objectFit: 'fill'},
+    naturalDimensions,
     ...clientSize,
-    ...naturalSize,
     ...props,
   };
 }
@@ -29,7 +29,7 @@ describe('Images: aspect-ratio audit', () => {
         ImageElements: [
           generateImage(
             {displayedWidth: data.clientSize[0], displayedHeight: data.clientSize[1]},
-            {naturalWidth: data.naturalSize[0], naturalHeight: data.naturalSize[1]},
+            {width: data.naturalSize[0], height: data.naturalSize[1]},
             data.props
           ),
         ],
@@ -81,7 +81,7 @@ describe('Images: aspect-ratio audit', () => {
     naturalSize: [800, 500],
     props: {
       isCss: false,
-      cssComputedObjectFit: 'cover',
+      computedStyles: {objectFit: 'cover'},
     },
   });
 
diff --git a/lighthouse-core/test/audits/image-size-responsive-test.js b/lighthouse-core/test/audits/image-size-responsive-test.js
index 153821b59..b1c943919 100644
--- a/lighthouse-core/test/audits/image-size-responsive-test.js
+++ b/lighthouse-core/test/audits/image-size-responsive-test.js
@@ -13,7 +13,7 @@ const assert = require('assert').strict;
 const WIDTH = 800;
 const HEIGHT = 600;
 
-function generateImage(clientSize, naturalSize, props, src = 'https://google.com/logo.png') {
+function generateImage(clientSize, naturalDimensions, props, src = 'https://google.com/logo.png') {
   const clientRect = {
     clientRect: {
       top: 0,
@@ -23,11 +23,11 @@ function generateImage(clientSize, naturalSize, props, src = 'https://google.com
     },
   };
   return {
-    cssComputedObjectFit: 'fill',
+    computedStyles: {objectFit: 'fill'},
     src,
     mimeType: 'image/png',
+    naturalDimensions,
     ...clientSize,
-    ...naturalSize,
     ...clientRect,
     ...props,
   };
@@ -41,7 +41,7 @@ describe('Images: size audit', () => {
         ImageElements: [
           generateImage(
             {displayedWidth: data.clientSize[0], displayedHeight: data.clientSize[1]},
-            {naturalWidth: data.naturalSize[0], naturalHeight: data.naturalSize[1]},
+            {width: data.naturalSize[0], height: data.naturalSize[1]},
             data.props
           ),
         ],
@@ -118,7 +118,7 @@ describe('Images: size audit', () => {
     clientSize: [100, 100],
     naturalSize: [5, 5],
     props: {
-      cssComputedObjectFit: 'cover',
+      computedStyles: {objectFit: 'cover'},
     },
   });
 
@@ -127,7 +127,7 @@ describe('Images: size audit', () => {
     clientSize: [100, 100],
     naturalSize: [5, 5],
     props: {
-      cssComputedImageRendering: 'pixelated',
+      computedStyles: {imageRendering: 'pixelated'},
     },
   });
 
@@ -388,15 +388,15 @@ describe('Images: size audit', () => {
       ImageElements: [
         generateImage(
           {displayedWidth: 80, displayedHeight: 40},
-          {naturalWidth: 40, naturalHeight: 20}
+          {width: 40, height: 20}
         ),
         generateImage(
           {displayedWidth: 160, displayedHeight: 80},
-          {naturalWidth: 40, naturalHeight: 20}
+          {width: 40, height: 20}
         ),
         generateImage(
           {displayedWidth: 60, displayedHeight: 30},
-          {naturalWidth: 40, naturalHeight: 20}
+          {width: 40, height: 20}
         ),
       ],
       ViewportDimensions: {
@@ -414,19 +414,19 @@ describe('Images: size audit', () => {
       ImageElements: [
         generateImage(
           {displayedWidth: 80, displayedHeight: 40},
-          {naturalWidth: 40, naturalHeight: 20},
+          {width: 40, height: 20},
           {},
           'image1.png'
         ),
         generateImage(
           {displayedWidth: 120, displayedHeight: 60},
-          {naturalWidth: 40, naturalHeight: 20},
+          {width: 40, height: 20},
           {},
           'image2.png'
         ),
         generateImage(
           {displayedWidth: 90, displayedHeight: 45},
-          {naturalWidth: 40, naturalHeight: 20},
+          {width: 40, height: 20},
           {},
           'image3.png'
         ),
@@ -447,7 +447,7 @@ describe('Images: size audit', () => {
       ImageElements: [
         generateImage(
           {displayedWidth: 80, displayedHeight: 40},
-          {naturalWidth: 40, naturalHeight: 20}
+          {width: 40, height: 20}
         ),
       ],
       ViewportDimensions: {
diff --git a/lighthouse-core/test/audits/unsized-images-test.js b/lighthouse-core/test/audits/unsized-images-test.js
index a5879fb21..6d87e45a2 100644
--- a/lighthouse-core/test/audits/unsized-images-test.js
+++ b/lighthouse-core/test/audits/unsized-images-test.js
@@ -10,8 +10,8 @@ const UnsizedImagesAudit = require('../../audits/unsized-images.js');
 /* eslint-env jest */
 
 function generateImage(props, src = 'https://google.com/logo.png', isCss = false,
-  isInShadowDOM = false, cssComputedPosition = 'static', node = {boundingRect: {}}) {
-  const image = {src, isCss, isInShadowDOM, cssComputedPosition, node};
+  isInShadowDOM = false, computedStyles = {position: 'static'}, node = {boundingRect: {}}) {
+  const image = {src, isCss, isInShadowDOM, computedStyles, node};
   Object.assign(image, props);
   return image;
 }
@@ -31,7 +31,7 @@ describe('Sized images audit', () => {
       isCss: true,
       attributeWidth: '',
       attributeHeight: '',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: null,
         height: null,
       },
@@ -44,7 +44,7 @@ describe('Sized images audit', () => {
       isInShadowDOM: true,
       attributeWidth: '',
       attributeHeight: '',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: null,
         height: null,
       },
@@ -54,10 +54,10 @@ describe('Sized images audit', () => {
 
   it('passes when an image has absolute css position', async () => {
     const result = await runAudit({
-      cssComputedPosition: 'absolute',
+      computedStyles: {position: 'absolute'},
       attributeWidth: '',
       attributeHeight: '',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: null,
         height: null,
       },
@@ -67,10 +67,10 @@ describe('Sized images audit', () => {
 
   it('passes when an image has fixed css position', async () => {
     const result = await runAudit({
-      cssComputedPosition: 'fixed',
+      computedStyles: {position: 'fixed'},
       attributeWidth: '',
       attributeHeight: '',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: null,
         height: null,
       },
@@ -80,10 +80,9 @@ describe('Sized images audit', () => {
 
   it('passes when an image is a non-network SVG', async () => {
     const result = await runAudit({
-      cssComputedPosition: '',
       attributeWidth: '',
       attributeHeight: '',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: null,
         height: null,
       },
@@ -98,7 +97,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
         },
@@ -110,7 +109,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: '100',
         },
@@ -122,7 +121,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: '100',
         },
@@ -134,7 +133,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: '100',
         },
@@ -149,7 +148,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
         },
@@ -161,7 +160,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: null,
         },
@@ -173,7 +172,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: null,
         },
@@ -186,7 +185,7 @@ describe('Sized images audit', () => {
     const result = await runAudit({
       attributeWidth: '',
       attributeHeight: '',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: null,
         height: null,
       },
@@ -199,7 +198,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: '100',
         },
@@ -211,7 +210,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
         },
@@ -223,7 +222,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: null,
         },
@@ -235,7 +234,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: '100',
         },
@@ -247,7 +246,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: '100',
         },
@@ -259,7 +258,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: null,
         },
@@ -271,7 +270,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: '100',
         },
@@ -283,7 +282,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: '100',
         },
@@ -295,7 +294,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: '100',
         },
@@ -307,7 +306,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '0',
         attributeHeight: '0',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
         },
@@ -325,7 +324,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
         },
@@ -345,7 +344,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '-200',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
         },
@@ -357,7 +356,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '-200',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
         },
@@ -369,7 +368,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: 'auto',
           height: '100',
         },
@@ -381,7 +380,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: 'auto',
         },
@@ -393,7 +392,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '-200',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: null,
         },
@@ -405,7 +404,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '-200',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: '100',
         },
@@ -417,7 +416,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: 'auto',
           height: '100',
         },
@@ -429,7 +428,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: 'auto',
         },
@@ -442,7 +441,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '100',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: 'auto',
           height: 'auto',
         },
@@ -455,7 +454,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '-200',
         attributeHeight: '-200',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: '100',
         },
@@ -468,7 +467,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '-200',
         attributeHeight: '-200',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: 'auto',
           height: 'auto',
         },
@@ -482,7 +481,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
           aspectRatio: '1 / 1',
@@ -495,7 +494,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
           aspectRatio: 'auto',
@@ -508,7 +507,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '100',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: null,
           height: null,
           aspectRatio: '1 / 1',
@@ -521,7 +520,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '100',
           height: null,
           aspectRatio: '1 / 1',
@@ -546,7 +545,7 @@ describe('Sized images audit', () => {
           {
             attributeWidth: '',
             attributeHeight: '',
-            _privateCssSizing: {
+            cssEffectiveRules: {
               width: null,
               height: null,
             },
@@ -564,7 +563,7 @@ describe('Sized images audit', () => {
           {
             attributeWidth: '',
             attributeHeight: '',
-            _privateCssSizing: {
+            cssEffectiveRules: {
               width: null,
               height: null,
             },
@@ -585,7 +584,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '',
         attributeHeight: '',
-        _privateCssSizing: undefined,
+        cssEffectiveRules: undefined,
       });
       expect(result.details.items.length).toEqual(0);
       expect(result.score).toEqual(1);
@@ -595,7 +594,7 @@ describe('Sized images audit', () => {
       const result = await runAudit({
         attributeWidth: '10',
         attributeHeight: '10',
-        _privateCssSizing: undefined,
+        cssEffectiveRules: undefined,
       });
       expect(result.details.items.length).toEqual(0);
       expect(result.score).toEqual(1);
diff --git a/lighthouse-core/test/gather/gatherers/image-elements-test.js b/lighthouse-core/test/gather/gatherers/image-elements-test.js
index ccbe9571a..c7f1d1f9c 100644
--- a/lighthouse-core/test/gather/gatherers/image-elements-test.js
+++ b/lighthouse-core/test/gather/gatherers/image-elements-test.js
@@ -48,17 +48,12 @@ function mockElement(partial = {}) {
     },
     attributeWidth: '',
     attributeHeight: '',
-    naturalWidth: undefined,
-    naturalHeight: undefined,
-    cssWidth: undefined,
-    cssHeight: undefined,
-    _privateCssSizing: undefined,
-    cssComputedPosition: 'absolute',
+    naturalDimensions: undefined,
+    cssEffectiveRules: undefined,
+    computedStyles: {position: 'absolute', objectFit: '', imageRendering: ''},
     isCss: false,
     isPicture: false,
     isInShadowDOM: false,
-    cssComputedObjectFit: '',
-    cssComputedImageRendering: '',
     node: {
       lhId: '__nodeid__',
       devtoolsNodePath: '1,HTML,1,BODY,1,DIV,1,IMG',
@@ -107,8 +102,10 @@ describe('.fetchElementsWithSizingInformation', () => {
 
     expect(driver._executionContext.evaluate).not.toHaveBeenCalled();
     expect(element).toEqual(mockElement({
-      naturalWidth: 200,
-      naturalHeight: 200,
+      naturalDimensions: {
+        width: 200,
+        height: 200,
+      },
     }));
   });
 
@@ -126,8 +123,10 @@ describe('.fetchElementsWithSizingInformation', () => {
       naturalHeight: 200,
     });
     expect(element).toEqual(mockElement({
-      naturalWidth: 200,
-      naturalHeight: 200,
+      naturalDimensions: {
+        width: 200,
+        height: 200,
+      },
     }));
   });
 
@@ -177,9 +176,7 @@ describe('.fetchSourceRules', () => {
     await gatherer.fetchSourceRules(session.asSession(), element.node.devtoolsNodePath, element);
 
     expect(element).toEqual(mockElement({
-      cssWidth: '200px',
-      cssHeight: '200px',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: '200px',
         height: '200px',
         aspectRatio: '1 / 1',
@@ -199,9 +196,7 @@ describe('.fetchSourceRules', () => {
     await gatherer.fetchSourceRules(session.asSession(), element.node.devtoolsNodePath, element);
 
     expect(element).toEqual(mockElement({
-      cssWidth: '200px',
-      cssHeight: '200px',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: '200px',
         height: '200px',
         aspectRatio: null,
@@ -238,9 +233,7 @@ describe('.fetchSourceRules', () => {
     await gatherer.fetchSourceRules(session.asSession(), element.node.devtoolsNodePath, element);
 
     expect(element).toEqual(mockElement({
-      cssWidth: '200px',
-      cssHeight: '200px',
-      _privateCssSizing: {
+      cssEffectiveRules: {
         width: '200px',
         height: '200px',
         aspectRatio: '1 / 1',
@@ -427,9 +420,7 @@ describe('FR compat', () => {
     expect(artifact).toEqual([
       mockElement({
         mimeType: 'image/jpeg',
-        cssWidth: '200px',
-        cssHeight: '200px',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '200px',
           height: '200px',
           aspectRatio: null,
@@ -462,9 +453,7 @@ describe('FR compat', () => {
     expect(artifact).toEqual([
       mockElement({
         mimeType: 'image/jpeg',
-        cssWidth: '200px',
-        cssHeight: '200px',
-        _privateCssSizing: {
+        cssEffectiveRules: {
           width: '200px',
           height: '200px',
           aspectRatio: null,
diff --git a/lighthouse-core/test/report/html/renderer/psi-test.js b/lighthouse-core/test/report/html/renderer/psi-test.js
index 1c7bf71a7..2199c8122 100644
--- a/lighthouse-core/test/report/html/renderer/psi-test.js
+++ b/lighthouse-core/test/report/html/renderer/psi-test.js
@@ -21,6 +21,8 @@ const CriticalRequestChainRenderer =
     require('../../../../report/html/renderer/crc-details-renderer.js');
 const ElementScreenshotRenderer =
     require('../../../../report/html/renderer/element-screenshot-renderer.js');
+const ReportUIFeatures =
+    require('../../../../report/html/renderer/report-ui-features.js');
 
 const {itIfProtoExists, sampleResultsRoundtripStr} = testUtils.getProtoRoundTrip();
 const sampleResultsStr = fs.readFileSync(__dirname + '/../../../results/sample_v2.json', 'utf-8');
@@ -48,6 +50,7 @@ describe('DOM', () => {
     global.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
     global.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
     global.ElementScreenshotRenderer = ElementScreenshotRenderer;
+    global.ReportUIFeatures = ReportUIFeatures;
 
     const {window} = new jsdom.JSDOM(TEMPLATE_FILE);
     document = window.document;
@@ -62,6 +65,7 @@ describe('DOM', () => {
     global.PerformanceCategoryRenderer = undefined;
     global.CriticalRequestChainRenderer = undefined;
     global.ElementScreenshotRenderer = undefined;
+    global.ReportUIFeatures = undefined;
   });
 
   describe('psi prepareLabData helpers', () => {
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index 7d0f576b5..8fc3e11b7 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -1181,6 +1181,54 @@
     }
   ],
   "ImageElements": [
+    {
+      "src": "http://localhost:10200/dobetterweb/lighthouse-rotating.gif",
+      "srcset": "",
+      "displayedWidth": 811,
+      "displayedHeight": 462,
+      "clientRect": {
+        "top": 657.8125,
+        "bottom": 1119.8125,
+        "left": 8,
+        "right": 819
+      },
+      "attributeWidth": "811",
+      "attributeHeight": "462",
+      "naturalDimensions": {
+        "width": 811,
+        "height": 462
+      },
+      "computedStyles": {
+        "position": "static",
+        "objectFit": "fill",
+        "imageRendering": "auto"
+      },
+      "isCss": false,
+      "isPicture": false,
+      "loading": "auto",
+      "isInShadowDOM": false,
+      "node": {
+        "lhId": "page-6-IMG",
+        "devtoolsNodePath": "3,HTML,1,BODY,24,IMG",
+        "selector": "body > img",
+        "boundingRect": {
+          "top": 658,
+          "bottom": 1120,
+          "left": 8,
+          "right": 819,
+          "width": 811,
+          "height": 462
+        },
+        "snippet": "<img src=\"lighthouse-rotating.gif\" width=\"811\" height=\"462\">",
+        "nodeLabel": "img"
+      },
+      "mimeType": "image/gif",
+      "cssEffectiveRules": {
+        "width": "811px",
+        "height": "462px",
+        "aspectRatio": "auto 811 / 462"
+      }
+    },
     {
       "src": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg?iar1",
       "srcset": "",
@@ -1192,16 +1240,20 @@
         "left": 8,
         "right": 128
       },
-      "naturalWidth": 480,
-      "naturalHeight": 318,
       "attributeWidth": "120",
       "attributeHeight": "15",
-      "cssComputedPosition": "static",
+      "naturalDimensions": {
+        "width": 480,
+        "height": 318
+      },
+      "computedStyles": {
+        "position": "static",
+        "objectFit": "fill",
+        "imageRendering": "auto"
+      },
       "isCss": false,
       "isPicture": false,
       "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "auto",
       "isInShadowDOM": false,
       "node": {
         "lhId": "page-0-IMG",
@@ -1219,11 +1271,10 @@
         "nodeLabel": "img"
       },
       "mimeType": "image/jpeg",
-      "cssWidth": "120px",
-      "cssHeight": "15px",
-      "_privateCssSizing": {
+      "cssEffectiveRules": {
         "width": "120px",
-        "height": "15px"
+        "height": "15px",
+        "aspectRatio": "auto 120 / 15"
       }
     },
     {
@@ -1237,16 +1288,20 @@
         "left": 132,
         "right": 252
       },
-      "naturalWidth": 480,
-      "naturalHeight": 318,
       "attributeWidth": "120",
       "attributeHeight": "80",
-      "cssComputedPosition": "static",
+      "naturalDimensions": {
+        "width": 480,
+        "height": 318
+      },
+      "computedStyles": {
+        "position": "static",
+        "objectFit": "fill",
+        "imageRendering": "auto"
+      },
       "isCss": false,
       "isPicture": false,
       "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "auto",
       "isInShadowDOM": false,
       "node": {
         "lhId": "page-1-IMG",
@@ -1264,34 +1319,37 @@
         "nodeLabel": "img"
       },
       "mimeType": "image/jpeg",
-      "cssWidth": "120px",
-      "cssHeight": "80px",
-      "_privateCssSizing": {
+      "cssEffectiveRules": {
         "width": "120px",
-        "height": "80px"
+        "height": "80px",
+        "aspectRatio": "auto 120 / 80"
       }
     },
     {
       "src": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg?isr1",
       "srcset": "",
-      "displayedWidth": 4800,
-      "displayedHeight": 3180,
+      "displayedWidth": 360,
+      "displayedHeight": 240,
       "clientRect": {
         "top": 573.8125,
-        "bottom": 3753.8125,
+        "bottom": 813.8125,
         "left": 252,
-        "right": 5052
+        "right": 612
+      },
+      "attributeWidth": "360",
+      "attributeHeight": "240",
+      "naturalDimensions": {
+        "width": 480,
+        "height": 318
+      },
+      "computedStyles": {
+        "position": "absolute",
+        "objectFit": "fill",
+        "imageRendering": "auto"
       },
-      "naturalWidth": 480,
-      "naturalHeight": 318,
-      "attributeWidth": "4800",
-      "attributeHeight": "3180",
-      "cssComputedPosition": "absolute",
       "isCss": false,
       "isPicture": false,
       "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "auto",
       "isInShadowDOM": false,
       "node": {
         "lhId": "page-2-IMG",
@@ -1299,21 +1357,20 @@
         "selector": "body > img",
         "boundingRect": {
           "top": 574,
-          "bottom": 3754,
+          "bottom": 814,
           "left": 252,
-          "right": 5052,
-          "width": 4800,
-          "height": 3180
+          "right": 612,
+          "width": 360,
+          "height": 240
         },
-        "snippet": "<img src=\"lighthouse-480x318.jpg?isr1\" width=\"4800\" height=\"3180\" style=\"position: absolute;\">",
+        "snippet": "<img src=\"lighthouse-480x318.jpg?isr1\" width=\"360\" height=\"240\" style=\"position: absolute;\">",
         "nodeLabel": "img"
       },
       "mimeType": "image/jpeg",
-      "cssWidth": "4800px",
-      "cssHeight": "3180px",
-      "_privateCssSizing": {
-        "width": "4800px",
-        "height": "3180px"
+      "cssEffectiveRules": {
+        "width": "360px",
+        "height": "240px",
+        "aspectRatio": "auto 360 / 240"
       }
     },
     {
@@ -1327,16 +1384,20 @@
         "left": 252,
         "right": 372
       },
-      "naturalWidth": 480,
-      "naturalHeight": 318,
       "attributeWidth": "120",
       "attributeHeight": "80",
-      "cssComputedPosition": "absolute",
+      "naturalDimensions": {
+        "width": 480,
+        "height": 318
+      },
+      "computedStyles": {
+        "position": "absolute",
+        "objectFit": "fill",
+        "imageRendering": "auto"
+      },
       "isCss": false,
       "isPicture": false,
       "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "auto",
       "isInShadowDOM": false,
       "node": {
         "lhId": "page-3-IMG",
@@ -1354,34 +1415,37 @@
         "nodeLabel": "img"
       },
       "mimeType": "image/jpeg",
-      "cssWidth": "120px",
-      "cssHeight": "80px",
-      "_privateCssSizing": {
+      "cssEffectiveRules": {
         "width": "120px",
-        "height": "80px"
+        "height": "80px",
+        "aspectRatio": "auto 120 / 80"
       }
     },
     {
       "src": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg?isr3",
       "srcset": "",
-      "displayedWidth": 960,
-      "displayedHeight": 636,
+      "displayedWidth": 360,
+      "displayedHeight": 240,
       "clientRect": {
         "top": 573.8125,
-        "bottom": 1209.8125,
+        "bottom": 813.8125,
         "left": 252,
-        "right": 1212
+        "right": 612
+      },
+      "attributeWidth": "360",
+      "attributeHeight": "240",
+      "naturalDimensions": {
+        "width": 480,
+        "height": 318
+      },
+      "computedStyles": {
+        "position": "absolute",
+        "objectFit": "fill",
+        "imageRendering": "pixelated"
       },
-      "naturalWidth": 480,
-      "naturalHeight": 318,
-      "attributeWidth": "960",
-      "attributeHeight": "636",
-      "cssComputedPosition": "absolute",
       "isCss": false,
       "isPicture": false,
       "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "pixelated",
       "isInShadowDOM": false,
       "node": {
         "lhId": "page-4-IMG",
@@ -1389,42 +1453,43 @@
         "selector": "body > img",
         "boundingRect": {
           "top": 574,
-          "bottom": 1210,
+          "bottom": 814,
           "left": 252,
-          "right": 1212,
-          "width": 960,
-          "height": 636
+          "right": 612,
+          "width": 360,
+          "height": 240
         },
-        "snippet": "<img src=\"lighthouse-480x318.jpg?isr3\" width=\"960\" height=\"636\" style=\"image-rendering: pixelated; position: absolute;\">",
+        "snippet": "<img src=\"lighthouse-480x318.jpg?isr3\" width=\"360\" height=\"240\" style=\"image-rendering: pixelated; position: absolute;\">",
         "nodeLabel": "img"
       },
       "mimeType": "image/jpeg",
-      "cssWidth": "960px",
-      "cssHeight": "636px",
-      "_privateCssSizing": {
-        "width": "960px",
-        "height": "636px"
+      "cssEffectiveRules": {
+        "width": "360px",
+        "height": "240px",
+        "aspectRatio": "auto 360 / 240"
       }
     },
     {
       "src": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg",
       "srcset": "lighthouse-480x318.jpg 2x",
-      "displayedWidth": 960,
-      "displayedHeight": 636,
+      "displayedWidth": 360,
+      "displayedHeight": 240,
       "clientRect": {
         "top": 573.8125,
-        "bottom": 1209.8125,
+        "bottom": 813.8125,
         "left": 252,
-        "right": 1212
+        "right": 612
+      },
+      "attributeWidth": "360",
+      "attributeHeight": "240",
+      "computedStyles": {
+        "position": "absolute",
+        "objectFit": "fill",
+        "imageRendering": "auto"
       },
-      "attributeWidth": "960",
-      "attributeHeight": "636",
-      "cssComputedPosition": "absolute",
       "isCss": false,
       "isPicture": false,
       "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "auto",
       "isInShadowDOM": false,
       "node": {
         "lhId": "page-5-IMG",
@@ -1432,68 +1497,24 @@
         "selector": "body > img",
         "boundingRect": {
           "top": 574,
-          "bottom": 1210,
+          "bottom": 814,
           "left": 252,
-          "right": 1212,
-          "width": 960,
-          "height": 636
+          "right": 612,
+          "width": 360,
+          "height": 240
         },
-        "snippet": "<img src=\"http://localhost:10200/dobetterweb/lighthouse-480x318.jpg\" srcset=\"lighthouse-480x318.jpg 2x\" width=\"960\" height=\"636\" style=\"position: absolute;\">",
+        "snippet": "<img src=\"http://localhost:10200/dobetterweb/lighthouse-480x318.jpg\" srcset=\"lighthouse-480x318.jpg 2x\" width=\"360\" height=\"240\" style=\"position: absolute;\">",
         "nodeLabel": "img"
       },
       "mimeType": "image/jpeg",
-      "cssWidth": "960px",
-      "cssHeight": "636px",
-      "_privateCssSizing": {
-        "width": "960px",
-        "height": "636px"
-      },
-      "naturalWidth": 480,
-      "naturalHeight": 318
-    },
-    {
-      "src": "http://localhost:10200/dobetterweb/lighthouse-rotating.gif",
-      "srcset": "",
-      "displayedWidth": 811,
-      "displayedHeight": 462,
-      "clientRect": {
-        "top": 657.8125,
-        "bottom": 1119.8125,
-        "left": 8,
-        "right": 819
-      },
-      "naturalWidth": 811,
-      "naturalHeight": 462,
-      "attributeWidth": "811",
-      "attributeHeight": "462",
-      "cssComputedPosition": "static",
-      "isCss": false,
-      "isPicture": false,
-      "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "auto",
-      "isInShadowDOM": false,
-      "node": {
-        "lhId": "page-6-IMG",
-        "devtoolsNodePath": "3,HTML,1,BODY,24,IMG",
-        "selector": "body > img",
-        "boundingRect": {
-          "top": 658,
-          "bottom": 1120,
-          "left": 8,
-          "right": 819,
-          "width": 811,
-          "height": 462
-        },
-        "snippet": "<img src=\"lighthouse-rotating.gif\" width=\"811\" height=\"462\">",
-        "nodeLabel": "img"
-      },
-      "mimeType": "image/gif",
-      "cssWidth": "811px",
-      "cssHeight": "462px",
-      "_privateCssSizing": {
-        "width": "811px",
-        "height": "462px"
+      "cssEffectiveRules": {
+        "width": "360px",
+        "height": "240px",
+        "aspectRatio": "auto 360 / 240"
+      },
+      "naturalDimensions": {
+        "width": 480,
+        "height": 318
       }
     },
     {
@@ -1507,16 +1528,20 @@
         "left": 322,
         "right": 338
       },
-      "naturalWidth": 0,
-      "naturalHeight": 0,
-      "attributeWidth": "",
-      "attributeHeight": "",
-      "cssComputedPosition": "static",
+      "attributeWidth": null,
+      "attributeHeight": null,
+      "naturalDimensions": {
+        "width": 0,
+        "height": 0
+      },
+      "computedStyles": {
+        "position": "static",
+        "objectFit": "fill",
+        "imageRendering": "auto"
+      },
       "isCss": false,
       "isPicture": false,
       "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "auto",
       "isInShadowDOM": false,
       "node": {
         "lhId": "page-7-IMG",
@@ -1533,9 +1558,10 @@
         "snippet": "<img src=\"blob:http://localhost:10200/822c70a0-b912-41c7-9a21-56c3d309e75b\">",
         "nodeLabel": "img"
       },
-      "_privateCssSizing": {
+      "cssEffectiveRules": {
         "width": null,
-        "height": null
+        "height": null,
+        "aspectRatio": null
       }
     },
     {
@@ -1549,20 +1575,24 @@
         "left": 8,
         "right": 24
       },
-      "naturalWidth": 0,
-      "naturalHeight": 0,
-      "attributeWidth": "",
-      "attributeHeight": "",
-      "cssComputedPosition": "static",
+      "attributeWidth": null,
+      "attributeHeight": null,
+      "naturalDimensions": {
+        "width": 0,
+        "height": 0
+      },
+      "computedStyles": {
+        "position": "static",
+        "objectFit": "fill",
+        "imageRendering": "auto"
+      },
       "isCss": false,
       "isPicture": false,
       "loading": "auto",
-      "cssComputedObjectFit": "fill",
-      "cssComputedImageRendering": "auto",
       "isInShadowDOM": false,
       "node": {
         "lhId": "page-8-IMG",
-        "devtoolsNodePath": "3,HTML,1,BODY,72,IMG",
+        "devtoolsNodePath": "3,HTML,1,BODY,73,IMG",
         "selector": "body > img",
         "boundingRect": {
           "top": 1317,
@@ -1575,9 +1605,10 @@
         "snippet": "<img src=\"filesystem:http://localhost:10200/temporary/empty-0.8423185960902964.png\">",
         "nodeLabel": "img"
       },
-      "_privateCssSizing": {
+      "cssEffectiveRules": {
         "width": null,
-        "height": null
+        "height": null,
+        "aspectRatio": null
       }
     }
   ],
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index da244e6c6..cbac6f1ed 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -684,14 +684,50 @@
     },
     "image-size-responsive": {
       "id": "image-size-responsive",
-      "title": "Serves images with appropriate resolution",
+      "title": "Serves images with low resolution",
       "description": "Image natural dimensions should be proportional to the display size and the pixel ratio to maximize image clarity. [Learn more](https://web.dev/serve-responsive-images/).",
-      "score": 1,
+      "score": 0,
       "scoreDisplayMode": "binary",
       "details": {
         "type": "table",
-        "headings": [],
-        "items": []
+        "headings": [
+          {
+            "key": "url",
+            "itemType": "thumbnail",
+            "text": ""
+          },
+          {
+            "key": "elidedUrl",
+            "itemType": "url",
+            "text": "URL"
+          },
+          {
+            "key": "displayedSize",
+            "itemType": "text",
+            "text": "Displayed size"
+          },
+          {
+            "key": "actualSize",
+            "itemType": "text",
+            "text": "Actual size"
+          },
+          {
+            "key": "expectedSize",
+            "itemType": "text",
+            "text": "Expected size"
+          }
+        ],
+        "items": [
+          {
+            "url": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg?isr1",
+            "elidedUrl": "http://localhost:10200/dobetterweb/lighthouse-480x318.jpg?isr1",
+            "displayedSize": "360 x 240",
+            "actualSize": "480 x 318",
+            "actualPixels": 152640,
+            "expectedSize": "720 x 480",
+            "expectedPixels": 345600
+          }
+        ]
       }
     },
     "preload-fonts": {
@@ -2091,7 +2127,7 @@
             "node": {
               "type": "node",
               "lhId": "page-8-IMG",
-              "path": "3,HTML,1,BODY,72,IMG",
+              "path": "3,HTML,1,BODY,73,IMG",
               "selector": "body > img",
               "boundingRect": {
                 "top": 1317,
@@ -5611,7 +5647,7 @@
         }
       ],
       "id": "best-practices",
-      "score": 0.25
+      "score": 0.19
     },
     "seo": {
       "title": "SEO",
@@ -7308,6 +7344,7 @@
       "lighthouse-core/lib/i18n/i18n.js | columnURL": [
         "audits[server-response-time].details.headings[0].label",
         "audits[image-aspect-ratio].details.headings[1].text",
+        "audits[image-size-responsive].details.headings[1].text",
         "audits[preload-fonts].details.headings[0].text",
         "audits[bootup-time].details.headings[0].text",
         "audits[network-rtt].details.headings[0].text",
@@ -7420,12 +7457,21 @@
       "lighthouse-core/audits/image-aspect-ratio.js | columnActual": [
         "audits[image-aspect-ratio].details.headings[3].text"
       ],
-      "lighthouse-core/audits/image-size-responsive.js | title": [
+      "lighthouse-core/audits/image-size-responsive.js | failureTitle": [
         "audits[image-size-responsive].title"
       ],
       "lighthouse-core/audits/image-size-responsive.js | description": [
         "audits[image-size-responsive].description"
       ],
+      "lighthouse-core/audits/image-size-responsive.js | columnDisplayed": [
+        "audits[image-size-responsive].details.headings[2].text"
+      ],
+      "lighthouse-core/audits/image-size-responsive.js | columnActual": [
+        "audits[image-size-responsive].details.headings[3].text"
+      ],
+      "lighthouse-core/audits/image-size-responsive.js | columnExpected": [
+        "audits[image-size-responsive].details.headings[4].text"
+      ],
       "lighthouse-core/audits/preload-fonts.js | failureTitle": [
         "audits[preload-fonts].title"
       ],