static async audit_(artifacts, networkRecords, context) {
    const {URL, SourceMaps} = artifacts;
    const images = artifacts.ImageElements;
    const viewportDimensions = artifacts.ViewportDimensions;
    const gatherContext = artifacts.GatherContext;
    const trace = artifacts.Trace;
    const devtoolsLog = artifacts.DevtoolsLog;

    /** @type {string[]} */
    const warnings = [];
    /** @type {Map<string, WasteResult>} */
    const resultsMap = new Map();
    for (const image of images) {
      const processed = OffscreenImages.computeWaste(image, viewportDimensions, networkRecords);
      if (processed === null) {
        continue;
      }

      if (processed instanceof Error) {
        warnings.push(processed.message);
        Sentry.captureException(processed, {tags: {audit: this.meta.id}, level: 'warning'});
        continue;
      }

      // If an image was used more than once, warn only about its least wasteful usage
      const existing = resultsMap.get(processed.url);
      if (!existing || existing.wastedBytes > processed.wastedBytes) {
        resultsMap.set(processed.url, processed);
      }
    }

    const settings = context.settings;

    let items;
    const unfilteredResults = Array.from(resultsMap.values());
    // get the interactive time or fallback to getting the end of trace time
    try {
      const metricComputationData =
        {trace, devtoolsLog, gatherContext, settings, URL, SourceMaps, simulator: null};
      const interactive = await Interactive.request(metricComputationData, context);

      // use interactive to generate items
      const lanternInteractive = /** @type {LH.Artifacts.LanternMetric} */ (interactive);
      // Filter out images that were loaded after all CPU activity
      items = context.settings.throttlingMethod === 'simulate' ?
        OffscreenImages.filterLanternResults(unfilteredResults, lanternInteractive) :
        // @ts-expect-error - .timestamp will exist if throttlingMethod isn't lantern
        OffscreenImages.filterObservedResults(unfilteredResults, interactive.timestamp);
    } catch (err) {
      // if the error is during a Lantern run, end of trace may also be inaccurate, so rethrow
      if (context.settings.throttlingMethod === 'simulate') {
        throw err;
      }
      // use end of trace as a substitute for finding interactive time
      items = OffscreenImages.filterObservedResults(unfilteredResults,
        await ProcessedTrace.request(trace, context).then(tot => tot.timestamps.traceEnd));
    }

    /** @type {LH.Audit.Details.Opportunity['headings']} */
    const headings = [
      {key: 'node', valueType: 'node', label: ''},
      {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
      {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
      {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
    ];

    return {
      warnings,
      items,
      headings,
    };
  }
@param {LH.Artifacts} artifacts
@param {Array<LH.Artifacts.NetworkRequest>} networkRecords
@param {LH.Audit.Context} context
@return {Promise<import('./byte-efficiency-audit.js').ByteEfficiencyProduct>}