static async createAuditProduct(result, simulator, metricComputationInput, context) {
    const results = result.items.sort((itemA, itemB) => itemB.wastedBytes - itemA.wastedBytes);

    const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);

    /** @type {LH.Audit.ProductMetricSavings} */
    const metricSavings = {
      FCP: 0,
      LCP: 0,
    };

    // `wastedMs` may be negative, if making the opportunity change could be detrimental.
    // This is useful information in the LHR and should be preserved.
    let wastedMs;
    if (metricComputationInput.gatherContext.gatherMode === 'navigation') {
      const optimisticFCPGraph = (await LanternFCP.request(metricComputationInput, context))
        .optimisticGraph;
      const optimisticLCPGraph = (await LanternLCP.request(metricComputationInput, context))
        .optimisticGraph;

      const {savings: fcpSavings} = this.computeWasteWithGraph(
        results,
        optimisticFCPGraph,
        simulator,
        {providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'fcp'}
      );
      // Note: LCP's optimistic graph sometimes unexpectedly yields higher savings than the pessimistic graph.
      const {savings: lcpGraphSavings} = this.computeWasteWithGraph(
        results,
        optimisticLCPGraph,
        simulator,
        {providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'lcp'}
      );


      // The LCP graph can underestimate the LCP savings if there is potential savings on the LCP record itself.
      let lcpRecordSavings = 0;
      const lcpRecord = await LCPImageRecord.request(metricComputationInput, context);
      if (lcpRecord) {
        const lcpResult = results.find(result => result.url === lcpRecord.url);
        if (lcpResult) {
          lcpRecordSavings = simulator.computeWastedMsFromWastedBytes(lcpResult.wastedBytes);
        }
      }

      metricSavings.FCP = fcpSavings;
      metricSavings.LCP = Math.max(lcpGraphSavings, lcpRecordSavings);
      wastedMs = metricSavings.LCP;
    } else {
      wastedMs = simulator.computeWastedMsFromWastedBytes(wastedBytes);
    }

    let displayValue = result.displayValue || '';
    if (typeof result.displayValue === 'undefined' && wastedBytes) {
      displayValue = str_(i18n.UIStrings.displayValueByteSavings, {wastedBytes});
    }

    const sortedBy = result.sortedBy || ['wastedBytes'];
    const details = Audit.makeOpportunityDetails(result.headings, results,
      {overallSavingsMs: wastedMs, overallSavingsBytes: wastedBytes, sortedBy});

    // TODO: Remove from debug data once `metricSavings` is added to the LHR.
    // For now, add it to debug data for visibility.
    details.debugData = {
      type: 'debugdata',
      metricSavings,
    };

    return {
      explanation: result.explanation,
      warnings: result.warnings,
      displayValue,
      numericValue: wastedMs,
      numericUnit: 'millisecond',
      score: results.length ? 0 : 1,
      details,
      metricSavings,
    };
  }
@param {ByteEfficiencyProduct} result
@param {LH.Gatherer.Simulation.Simulator} simulator
@param {LH.Artifacts.MetricComputationDataInput} metricComputationInput
@param {LH.Audit.Context} context
@return {Promise<LH.Audit.Product>}